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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Posted by 사무엘

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

1. 호출과 사용

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Posted by 사무엘

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

1. 코드의 입체적 배치

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

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

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

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

2. 컴파일러의 경고

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

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

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

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

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

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

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

3. #include 절대경로 표시

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

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

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

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

4. 리터럴 형태의 표현

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

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

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

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

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

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

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

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

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

5. 기타

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

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

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

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

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

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

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

Posted by 사무엘

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Posted by 사무엘

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

1. 동영상

유튜브가 광고 없는 유료 프리미엄 서비스를 밀고 홍보하기 시작하더니, 2019년 하반기쯤부터는 반대로 일반 무료 상태에서는 광고가 예전보다 더 길어지고 잦아진 것을 다들 느끼실 것이다. 원래 5초 1회로 시작했던 것이 6~7초 2회로 늘었다. 요런 걸로 유료 가입자를 확보하고 수익을 내려는가 보다.

하긴, 유튜브는 인터넷에서 깨진 동영상 링크라는 걸 없애고, 오프라인 다운로드가 당연하다고 여겨졌던 60프레임 HD급 초고화질 동영상까지 실시간 스트리밍으로 실현한 엄청난 사이트이다. 모니터 주사율만 해도 평소에 50~60Hz를 쓰다가 75Hz이상을 맞추면 화면이 훨씬 더 부드러워진 게 느껴지는데.. 동영상도 60프레임짜리를 보면 화질을 떠나서 움직임이 확~ 더 자연스럽고 부드럽고 보기 편한 게 티가 난다.

쟤는 그냥 지구상에 존재하는 모든 영상 기록들의 아카이브인 동시에 전세계의 개인 방송, 교회 설교 같은 것까지 몽땅 감당하고 있다. 집에 TV가 없어도 유튜브가 TV나 마찬가지이다. 동영상을 하나 보기 시작했다가 같이 뜨는 관련 동영상까지 섭렵하다 보면 시간 가는 줄 모른다.

이런 괴물이 등장할 거라고는 198, 90년대의 그 어떤 SF 작가도 예상하지 못했으며..
저걸 돌리려면 평소에 서버 유지비도 정말 장난 아니게 들지 싶다.;;; 이걸 버틸 재간이 안 되는 국내의 중소 동영상 포털들은 수지가 안 맞으니 2010년대 초· 중반을 못 넘기고 다들 망했다.

한 20여 년 전에 컴퓨터에서 동영상이라는 건 그냥 320*240의 자그마한 화면에서 노이즈 대신 JPG artifact가 가득한 허접한 화질로 보는 avi, mpg, mov가 전부였다. 전반적인 화질은 기존 VHS 비디오 테이프보다 결코 좋을 게 없고, 단지 아날로그가 아닌 디지털이라는 의의만 있을 뿐이었다.
1990년대 초-중까지는 MPEG1/2급 동영상을 시청하기 위해서 별도의 그래픽 가속 하드웨어를 장착해야 할 정도였다. 동영상 캡처/인코딩이 아니라 단순 시청을 위해서도 말이다. 이런 가속은 통상적인 3D 그래픽용 가속과는 별개의 분야일 것이다.

그러다가 한 2000년경엔 갑자기 온갖 코덱들이 난립하기 시작해서 통합 코덱 패키지가 나오고, '사사미'라고 자막이 화면에 깔끔하게 입혀진 채로 뜨는 새끈한 동영상 재생기가 개발되었다. 이때까지만 해도 고화질 영화· 애니 동영상은 각종 P2P 불법 공유 네트워크를 통해 많이 오갔던 것 같다.
온라인 실시간 스트리밍으로는 어림도 없고.. 유튜브만 해도 2010년대 이전에는 여전히 3~400대 해상도에 머물러 있었으며 동영상 하나당 10분 시간 제한까지 있었다. 지금으로서는 믿어지지 않을 것이다.

난 동영상 압축 알고리즘에 대해서는 아는 게 별로 없다. 인접한 프레임, 그리고 인접한 픽셀들이 서로 유사하다는 것을 이용해서 차이점만 담는다는 것이 골자일 텐데.. 이건 컴퓨터에서 캐시의 개념을 설명할 때 언급하는 시간 지역성, 공간 지역성하고 비슷한 개념 같다. 그나마 동영상 재생 및 압축 기술이 다들 대인배 오픈소스로 풀려 있기 때문에 사람들이 폰이나 PC에서 더 저렴하고 간편하게 동영상을 즐길 수 있다.

2. CPU의 다양다변화, 병렬화

21세기에 컴퓨터 CPU에서 단일 코어 클럭 속도의 '기하급수' 증가가 드디어 멈췄다. 그 대신 지금까지 슈퍼컴퓨터 세계의 전유물로 여겨졌던 멀티코어가 개인용 PC에도 등장하게 됐다. 이게 1차 변화이다.

그래서 어느 플랫폼에서나 동일한 방식으로 스레드를 생성하라고, CPU의 여러 코어를 잘 제어하고 활용하라고 OpenMP라는 규격이 제정되기도 했다. 이건 특정 연산에 대해서 적당히 병렬화하라고 지시를 내리는 여러 C 함수와 언어 확장, #pragma들의 모음이다. 난 지금까지 UI의 반응성 향상을 위해서만(= 백그라운드 작업) 스레드를 사용해 왔지, CPU 자원을 몽땅 쪽쪽 빨아서 쓰기 위해서 스레드를 동원해 본 적은 없었다.

요즘 컴퓨터들은 뺑뺑이를 돌려 봤자 전체 CPU 사용량이 10%대밖에 되지 않는 건 사실이다. 하지만 그렇다고 해서 별다른 최적화 없이 무턱대로 스레드를 만들어서 CPU 사용량을 늘리면 그래 봤자 throughput이 그저 전체 CPU 사용량에 비례해서 팍팍 오르는 것은 확실히 아니어 보인다.

작업 스레드를 만들면 각 작업들의 수행 속도도 눈에 띄게 느려지기 때문이다. 이는 어지간한 하이엔드급 컴에서도 관찰 가능한 현상이다. 뭐랄까, 작업 스레드가 증가하면 context switching 같은 다른 오버헤드도 추가되어서 전체 효율이 떨어지는 것 같다.

프로그램을 너무 많이 띄워서 메모리가 부족해지면 가상 메모리 페이징(디스크 스와핑)만 죽어라고 하느라 컴퓨터의 작업 수행 속도가 급락한다. 좁은 화면에 창을 너무 많이 띄우면 사람도 창 전환만 하느라 작업 능률이 급락하며, 반대로 모니터를 여러 개 연결하는 건 생산성을 사기급으로 향상시킨다.
이와 마찬가지로 CPU의 성능에 걸맞지 않게 작업 스레드가 너무 많아지만.. 작업 전환 비용의 증가만으로 성능이 극도로 떨어질 수 있겠다.

그런데 요즘은 그걸로 끝이 아니다. 게임용 3D 그래픽 렌더링이나 좀 보조하라고 만들어졌던 add-on인 GPU도 거기에만 쓰는 게 아깝다. 굳이 3D 그래픽이 아니어도 그것처럼 단순무식하고 물량빨인 계산 작업이 있으면 거기에도 GPU를 투입할 수 있다. 특히 살인적인 노가다 계산이 필요한 머신러닝 분야에서 GPU 연산이 더욱 각광받기 시작했다.

그러니 이것도 별도의 프로그래밍 영역이 됐다. nVIDIA에서 GPU 활용 프로그래밍을 위해 OpenMP와 비슷한 컨셉으로 CUDA라는 라이브러리를 내놓았다. 이건 그냥 인텔 내장 그래픽을 쓰는 노트북 같은 기계에서는 활용할 수 없다는 것이 멀티코어 CPU와 다른 점이다. 그것 말고 OpenCL 같은 다른 GPU 라이브러리도 등장했다.

하긴, 아직 싱글코어 시절일 때도 아까 얘기했던 동영상 정도의 멀티미디어 처리를 위해서 인텔에서는 SIMD(1명령 다중 데이터) 정도의 병렬 처리를 지원하는 명령을 도입한 적이 있었다. 그게 옛날 1990년대 말의 펜티엄 프로~III 기간에 추가된 MMX 내지 SSE 명령이다. 얘가 기존 x87 명령을 대신해서 부동소수점 연산까지 처리한다.

옛날에 하드웨어의 성능을 극한으로 짜내는 게임을 만들려면 어셈블리어를 알아야 하고 비디오 메모리에 직통으로 내용을 직접 뿌린다거나 해야 했다. 지금은 특정 CPU의 인스트럭션을 써 주는 짓은 필요 없지만, 그것과는 좀 다른 양상으로 하드웨어를 직접 다루는 최적화 테크닉이 필요한 시대가 되어 있다.

3. 슈퍼컴퓨터 내지 CPU 아키텍처의 명멸

본인은 어린 시절에 슈퍼컴퓨터라고 하면 덩치가 크고, 내부에 무슨 금칠이라도 했는지 가격이 억소리 나게 비싸면서.. 개인용 PC보다 메모리가 훨씬 더 방대하고 반응 속도가 수십 수백 배 정도 더 빠른 물건 정도로나 생각했다.
뭐, 20세기 옛날에는 개인용 컴퓨터 대비 전용 슈퍼컴의 차이가 그런 단순한 차이점에 더 근접해 있기도 했다.

하지만 오늘날은 개인용 PC가 10~20년 전의 업계 최고의 슈퍼컴보다 더 빠르다. 마치 오늘날의 무선 인터넷이 10~20년 전의 유선 인터넷보다 더 빠르듯이.. 정말 경이로운 노릇이다.
이제 슈퍼컴이라고 해서 개인용 PC보다 단순히 기계적으로 무식하게 더 빠르지는 않다. "개인용 PC가 64비트 3~4GHz대니까 슈퍼컴은 machine word가 256비트이고 클럭 속도는 40GHz 정도 하겠지"가 아니라는 뜻이다.

이제는 슈퍼컴 전용 아키텍처, 전용 운영체제 같은 것도 존재하지 않으며, 비트 수가 차이 나는 것도 아니고.. 다 똑같이 x86이다.
차이가 나는 건 계산을 위한 코어수뿐이다. 그걸 정교하게 제어하는 별도의 프로그램을 짜서 돌려야 슈퍼컴의 성능을 제대로 활용할 수 있다.

단일 코어의 클럭 속도는 이제 개인 PC도 슈퍼컴과 비슷하거나 더 나으면 낫지, 결코 뒤쳐지지 않는다.
그냥 단일 코어만 열나게 돌리는 PC용 PI 계산 프로그램을 그대로 돌리면 슈퍼컴이라고 해서 몇백만 자리가 즉시 짠~ 하고 튀어나오지 않는다.

옛날의 전용 패러다임과 재래식 생산 공정 하에서 슈퍼컴을 열심히 연구 개발했던 크레이 같은 공돌이가 오늘날의 컴퓨팅 환경을 본다면 놀라서 까무러치지 않을까 싶다.
통상적인 시뮬레이션· 계산용 슈퍼컴은.. 단순히 외부로부터의 대용량 트래픽 처리용인 고성능 서버하고는 지향하는 게 다르다. 스포츠 사격과 군대 사격이 다른 것만큼이나 다르다고 생각하면 될 듯하다. IBM 메인프레임은 둘 중 어느 용도에 더 가까운 걸까..?

오늘날은 PC가 성능이 워낙 향상되어서 PC와 슈퍼컴 사이의 경계 축에 들던 '워크스테이션'이라는 컴퓨터 체급도 의미가 많이 퇴색했다. 굳이 따지자면 맥 프로 같은 high-end급 PC일 뿐이겠지.
그 시절에 워크스테이션이란 운영체제도 OS/2나 솔라리스, NextStep, Windows NT 정도 돌리던 전문가 업무용 컴터였으며 가격은 거의 경차 한 대에 육박했었다. 노는 물이 도스나 Windows 3.1 따위하고는 완전히 달랐다.

이런 식으로.. 데스크톱 PC는 호환성이 깡패인 x86+x64 천지가 됐고, 모바일은 ARM인데 얘들도 PC로 호시탐탐 진출하려고 노력하는 중.. 거기에다 게임기는 아직 PowerPC가 살아 있나 모르겠고, 메인프레임에 IBM POWER 정도가 살아 있는 것 같다.
이젠 구글과 애플도 CPU를 직접 만들려고 하고.. 과거 Windows NT 3~4 시절과는 다른 의미의 CPU 아키텍처 춘추 전국 시대가 열리는 게 아닌가 모르겠다. 결국은 이식성 좋게 만들어진 소프트웨어가 승자가 된다.

4. 전자와 전산의 관계

가만히 생각해 보면.. C++ 언어로 Windows MFC 등.. 상대적으로 '특정 플랫폼 실무에 가까운 프로그래밍'을 자주 하는 사람은 전산· 컴공보다는 전자공학 쪽에 더 많았던 것 같다. 과거에 유명한 Visual C++ 프로그래밍 베스트셀러 책을 집필했던 분들도 전공이 그런 쪽이었다. 세부적으로는 로봇 공학이라든가 디지털 신호 처리 쪽으로 말이다.

그럼 진짜 전산· 컴공을 한 사람들은 뭘 하는가 하면.. 좀 더 크로스 플랫폼이나 오픈소스에 친화된 스타일의 코딩을 한다. 뭐, 웹 프로그래밍도 방법론은 사뭇 다르지만 크로스 플랫폼 프로그래밍의 범주에 들 테고..
특히 PL 쪽 덕후들은 C++ 같은 지저분한(?) 언어는 거들떠보지도 않고 Rust나 go 같은 더 마이너한 언어, 함수형 언어 같은 걸 즐겨 쓴다. 쉽게 말해 더 추상적이고 고차원적인(?) 걸 추구하는 듯하다.

아 그렇다고 모든 전자과 출신이나 모든 전산과 출신이 취향이 그렇게 갈린다는 말은 물론 아니다.
그리고 신호 처리는 전자공학이지만 컴퓨터그래픽스는 명백하게 전산학의 세부 분야인 것 같다. 요컨대 영상을 렌더링 하는 건 전산학이고, 그 생성된 영상을 손실 압축해서 저장하는 건 전자공학 쪽인 셈이다. 다음으로 영상 인식 같은 건 전자와 전산이 별 구분 없이 같이 파는 것 같다.

Posted by 사무엘

2020/02/16 08:36 2020/02/16 08:36
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1717

1. 날개셋 한글 입력기 9.9

이미 확인한 분도 계시겠지만 날개셋 한글 입력기의 차기 버전인 9.9가 지난주, 지난 1월 말에 완성되고 공개되었다.

버전 9.9와 10.0 중에서 고민하던 끝에 아쉽지만 9.9를 선택했다. 비록 9.8x 이후로 많은 작업이 진행되고 많은 것이 개선되긴 했지만 외형은 지난달에 올렸던 개발 근황 이후로 크게 달라진 게 없기 때문이다. 그래서 새 버전 소식도 이렇게 소박하게(?) 전하고자 한다.
9.9라는 숫자에는 본인의 그런 아쉬운 심정이 담겨 있다. 그래도 얘는 9.x대의 마지막 버전이며, 진짜 10.0이 한 3월 말쯤으로 계획돼 있다.

(1) 9.82에서 프로그램별 수동 보정 기능이 추가됐는데, 몇몇 사용자 분들에게서 온 피드백을 들어 보면 그게 실제로 도움이 된 듯하다. 그거 설정을 바꾸는 것으로 새로운 프로그램에서의 오동작을 해결했기 때문이다. (예: Visual Studio Code 에디터)

(2) 한편, 크롬 브라우저가 버전 78에서는 자신이 데스크톱 앱인데도 IME에다가는 메트로 앱이라고 알려주는 버그가 있어서 9.82 당시에는 이를 임의로 보정하는 설정이 들어갔었다. 하지만 지금의 79에서는 그게 고쳐졌기 때문에 날개셋에서도 보정 설정이 제거되었다. 하지만 보정을 하더라도 딱히 다른 문제나 부작용은 없다.

그러므로 현재로서는 본인이 아는 한도에서는 “강제로 데스크톱 앱으로 동작” 보정이 필요한 프로그램이 없다는 것을 도움말에도 언급해 놓았다. 그냥 미래에 또 이런 일이 발생할 수 있다는 걸 염두에 두고 보정 설정을 남겨 뒀다.

공식적으로 문서화되지 않은 변화 사항으로는 남은 메모리 양을 표시할 때 내가 직접 단위 계산을 하는 게 아니라 운영체제의 깔끔한 API를 쓰게 한 것, 변환기 대화상자가 프로그램을 실행시킨 쪽의 모니터에서 표시되게 한 것, 인코딩 목록에서 UTF-7은 이제 거의 쓰이지 않으니 맨 뒤로 밀어낸 것 등.. 아주 사소한 것 위주이다.

그런 것 말고 좀 유의미한 작업이 진행된 것도 있는데, “조합과 후보 자동 완성”과 “조합 안에 조합 생성” 입력 도구에서 각종 후보 목록은 백그라운드 스레드에서 생성될 수 있게 내부 공사를 진행해 놓았다. 전자는 타이핑에 랙을 야기하지 않으면서 목록이 다 완성되면 한꺼번에 짠 표시하는 것만 담당하지만, 후자는 마치 웹 페이지 로딩하듯이 일단 자그마한 목록부터 띄운 뒤에 후보를 여기저기 incremental하게 실시간으로 추가하는 것까지 가능하다. 다만, 이것도 이를 실제로 활용하는 기능이 아직 없기 때문에 존재감이 없다.

입력기에 적용된 사소한 개선 사항이 타자연습에도 같이 반영된 것이 있긴 하지만.. 너무 사소하고 자잘한 것이기 때문에 타자연습은 아직 정식으로 버전업을 하지 않았다. 이번 9.9는 타자연습 3.9와도 API가 호환되니 그대로 같이 사용 가능하다.

새 버전을 유용히 사용하시기 바란다. 페이스북 플러그인은 고장난 지 한참 됐기 때문에 프로그램 다운로드 페이지에서도 완전히 제거했다. 그래서 본인의 이메일 주소만 기재해 놓았다.

2. 레거시 프로젝트 파일 정리

새해 기념으로.. 날개셋 한글 입력기의 소스에서 구닥다리 구버전 Visual C++용 솔루션/프로젝트 파일들을 드디어 완전히 삭제했다. 이를테면 *.vcproj (200x용), 그리고 심지어 *.dsp/*.dsw (6!!) 말이다.

사실, 소스 코드에 C++11 문법을 도입하던 순간부터 내 프로젝트들은 VC++ 2010 이전 버전과 연을 완전히 끊은 거나 마찬가지였다.
그리고 VC++ 역시 딱 2010부터 지금과 같은 솔루션/프로젝트 파일과 버전 관리 체계가 정착했고, IDE와 컴파일러 툴킷, 플랫폼 SDK 계층이 깔끔하게 분리되기도 했다. 그러니 그 이전 버전은 이제 신경 쓸 필요가 없다.

본인의 개인적인 소신은 더 쓰이지 않는 파일이어도 요즘이 하드 공간이 부족한 시대도 아니고, "굳이 일부러 찾아서 지우는 수고까지 할 필요는 없다" 주의였다. 하지만, 그것들이 보는 사람을 괜히 헷갈리게 하고 무질서도를 높이는 부작용에 대해서는 지금까지 생각을 못 하고 있었다.

이건 물건 정리와도 비슷하다. 언젠가는 다시 쓸 일이 생길지도 모르는 물건이 분명 있겠지만.. 너님의 생활 습관상 그럴 일 없으니 좀 버려야 하는 물건도 있다.
비슷한 맥락에서.. 사용되지 않는 코드도 무작정 주석이나 #if 0 처리만 해서 누더기처럼 덕지덕지 남겨 두는 게 장땡이 아니다. 재사용할 가능성이 정말 희박하고 남 보기에 정신 사납게 하는 역효과가 더 큰 것들은 그냥 완전히 지워 없애 버리는 미덕을 발휘하는 것도 필요해 보인다. 그 '정도'와 경계는 개인 취향에 달린 문제이겠지만 말이다..

아울러, 소스 코드들이 DB 데이터라면, 이들을 빌드하는 방식을 명시하는(각종 컴파일러· 링커 옵션들) 프로젝트 및 복잡한 빌드 스크립트는 DB 스키마 또는 아주 복잡한 쿼리와 비슷한 물건일 것이다. 이것도 날렸다가 다시 구성하는 건 소스 코드 자체를 날리는 것 만만찮게 골치아픈 일이 될 것이다.

3. 3D 그래픽 시연 프로그램

끝으로.. 최근에 이걸 만들어 봤다.
3D 컴퓨터그래픽을 공부하는 사람이라면 모를 수가 없는 그 유명한 '유타 주전자'를 3차원 그래픽 시연 프로그램의 예제 데이터로 추가했다. 이게 지난 10여 년 동안 제공되지 않고 있었다니.. 송구스럽기가 이루 말할 수 없을 지경이다.

이 주전자를 구성하는 3D 좌표 데이터야 이미 대외적으로 널리 공개돼 있다. 하지만 이걸 3차원 그래픽 시연 프로그램이 곧장 읽을 수 있는 무식한 직선의 나열로 변환하려면 베지어 곡선을 넘어 베지어 곡면이라는 것을 적당히 근사해서 와이어프레임 형태로 표현할 수 있어야 한다.

3차 베지어 곡선이 4개의 점(시작점 + 끝점 + 제어점 2개)으로 구성되고 t=0..1 사이의 인자를 받는 매개변수 함수로 표현된다면..
3차 베지어 곡면은 그런 베지어 곡선을 4개나 모아서 평면을 이루며, 0..1 사이의 인자 매개변수도 하나가 아니라 2차원답게 둘을 받는다.

전자가 함수값을 구하기 위해 계수와 제어점 사이의 곱셈과 덧셈을 4회 수행한다면, 후자는 그 제곱인 16회나 수행한다. 아니, 각각의 항 자체도 계수*제어점이 아니라 계수x*계수y*제어점으로 곱셈의 횟수가 더 많다. 계산량이 정말 장난이 아니더라.

이 세상의 많고 많은 글꼴들이 모두 베지어 곡선으로 표현되듯, 자동차나 비행기처럼 인간이 디자인한 기계류의 그 '유체역학적인' 부드러운 곡면도 다 이 공식을 이용해서 기술된다. 옛날에 베지어 곡선이라는 걸 고안한 사람인 '피에르 베지어'가 직업이 무엇이었는지를 생각해 보면 답이 명확해진다.

암호 같은 베지어 곡면을 인터넷을 통해 알게 된 공식대로 직선들로 쫙 풀어서 표현해 주니.. 내 프로그램에서 '유타 주전자'의 와이어프레임이 거짓말처럼 짠 나타났다. 정말 신기했다.
이런 유형의 계산은 양만 많지 패턴이 워낙 규칙적이니, 캐시 적중률 높고 병렬화에도 유리하다. GPU가 괜히 진작부터 만들어져 쓰인 게 아닐 것이다.

일반적인 3D 그래픽 렌더러라면 내 프로그램처럼 가냘픈 선이 아니라 폴리곤을 기본 단위로 취급할 것이고, 와이어프레임조차도 폴리곤을 기반으로 렌더링 방식만 변경해서 표시하는 것일 테니 삼각형 단위로 선들이 더 조밀하게 나타날 것이다. 하지만 내 프로그램은 그냥 베지어 평면의 각 격자 단위로만 선을 그었기 때문에 기본 단위가 사각형 형태로 나타난다.

정말 신기하게도.. 이 유타 주전자 데이터를 구성하는 선의 개수와,
기존 예제 중에서 원형 튜브(Torus)의 선의 개수가 서로 정확하게 일치한다. 2304개이다.
둘은 내부 구조나 데이터 생성 방식이 서로 완전히 다르고 관련이 없는데도 말이다.

Posted by 사무엘

2020/02/02 19:33 2020/02/02 19:33
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1712

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

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

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

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

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

사용자 삽입 이미지

사용자 삽입 이미지

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Posted by 사무엘

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

요즘 Visual C++ 사용 메모

1. 디버깅 관련

수 년 전에 본인은 Windows에서 명령 프롬프트와 디버그 로그(OutputDebugString)에 유니코드가 지원되는 날이 언제쯤 올까 푸념을 늘어놓은 적이 있었는데.. 이건 놀랍게도 Windows 10에서 명령 프롬프트의 유니코드화(특수한 여건이 갖춰졌을 때 부분적으로 한해서)와 더불어 그럭저럭 현실이 됐다.

디버거 툴에 대해서 본인이 더 원하는 것은..

(1) IDE가 디버거를 붙여서 직접 실행해 준 디버기 말고.. 타 프로세스에 의해 실행된 디버기도 자동으로 감지해서 breakpoint 내지 로그 출력을 잡아 주기
(2) breakpoint의 작동 조건으로, "임의의 타 지점을 먼저 지나쳤거나 그게 call stack 아래에 있을 것" 정도 지정하기

정도이다.
(1)을 위해서 Attach to process 같은 기능이 이미 있긴 하다. 하지만 내 프로그램이 아주 잠깐 동안만 짤막하게 실행되고 마는 상황이라면(정상적인 종료이든, 오류로 인한 종료이든) 사용자가 느릿느릿 일일이 저 명령을 내릴 겨를이 없다.
이건 EXE의 디버깅도 DLL의 디버깅과 비슷한 양상으로 만든다. 실행 인자를 사용자가 지정해 주는 게 아니라, 이 EXE는 다른 EXE로부터 어떤 인자를 받아서 실행됐는지를 디버거로부터 안내받게 될 것이다.

(2)는 물론 코드 자체를 고쳐서 상태 변수 같은 걸 global하게 추가하는 식으로 편법으로 구현할 수는 있다. 하지만 그건 몹시 귀찮고 불편하다.
디버깅을 해야 하는 코드가 여러 부분에서 호출되고 있는데 우리는 특정 상황에서 호출된 것에만 관심이 가 있는 거.. 생각보다 자주 있는 일이다. 이에 대한 지원이 더 잘 된다면 프로그래머의 생산성이 많이 향상될 수 있을 것이다.

글쎄, 위의 두 아이템은 오래 전에 이미 언급한 적도 있을 것이다.
이것 말고.. 딱히 기술적으로 어려울 것 전혀 없는데 좀 있었으면 좋겠다 싶은 기능으로는..
디버깅을 위해 실행할 프로그램과 인자(argument)를 여러 세트 등록해 놓고.. 사용자가 예전에 등록해 놨던 세트를 곧장 불러올 수 있으면 좋겠다.

지금도 Debug 탭의 Command 입력란의 콤보 상자를 눌러 보면.. 달랑 revsvr32, Edit, Browse 이런 몇 가지 고정적인 아이템밖에 없다. 거기에다가 사용자가 이전에 등록한 적 있는 세트들이 같이 나오면 된다. 이 얼마나 깔끔한가?
EXE라면 Command가 바뀔 일은 별로 없겠지만 인자에 대한 세트 관리 기능이 있다면 충분히 유용할 수 있다.
IDE에 이런 기능이 없으니 날개셋 같은 개인 작품에서나 회사 제품 코드에서나.. 디버깅을 위해 사용할 다양한 프로그램들 경로를.. 소스 코드 주석이나 별도의 텍스트 파일에다 따로 메모해 놓는 촌극이 벌어지고 있다.

세트 데이터는 굳이 해당 프로젝트 파일에다가 저장하지 않아도 된다. 프로젝트/솔루션에 의존할 필요 없이, 그냥 그 프로그램 자체의 history data 명목으로 관리하는 형태로 제공되어도 충분히 편리할 것 같다.

2. 코드 자동 서식 적용

요즘 Visual C++ IDE에는.. 코딩을 하면서 닫는 중괄호나 세미콜론이 입력됐을 때, 각종 변수와 연산자· 토큰 사이에 공백을 균일하게 삽입하거나 없애고 탭 들여쓰기도 일관되게 맞춰 주는 '자동 서식' 기능이 제공된다. 쉽게 말해 whitespace에 대한 formatting 말이다. 이 옵션이 기본적으로 켜져 있다.

내 기억이 맞다면 이건 Visual C++ 2013쯤부터 처음으로 도입됐다. 2012에는 아직 확실하게 없었다.
베이직은 1980년대 도스 시절 QuickBasic에서부터 있었으며 C#도 최소한 200x 버전에서는 들어간 기능이지 싶은데 C++은 이제야 도입됐다.

다른 언어들은 문장을 완전히 파싱해서 내부 representation tree로 바꾼 뒤, 그걸 텍스트로 재구성함으로써 서식도 덤으로 적용되는 것이겠지만, C++은 그럴 수는 없지 싶다. 진짜 기계적이고 lexical한 문자열 치환 수준에서만 서식이 적용되지 싶다.

자동 서식 기능이 전반적으로는 괜찮은 편인데.. int *a, *b는 왜 int* a, * b라고 공백을 어색하게 배치하나 모르겠다. D처럼 int* a,b라고 썼을 때 b까지 포인터형이 되는 언어라면 모를까, 포인터형 별표와 변수명 사이에 공백이 들어가야 할 필요는 느껴지지 않는다.

그리고 배열 delete인 delete[]도 토큰 배치가 약간 기괴하긴 하지만.. 개인적으로는 붙여서 delete[] ptr; 이러는 걸 선호한다. 거기까지는 괜찮은데 delete []a를 다 붙여서 delete[]a로 바꾸는 건 좀 의아하다. 차라리 delete[] a라고 해 주지..
비슷한 맥락으로로, 함수의 인자로 배열의 포인터를 전달하는데 TYPE(*arg)[4] 같은 것을 한데 다 붙여 버리니 이 또한 어색하고 이상하다.

이런 것들이 C++의 자동 서식은 완전한 파싱을 거쳐서 적용되는 게 아니기 때문에 발생하는 부작용이지 싶다. 그러니 매크로나 템플릿 내부 같은 데서도 정확한 동작을 기대하기 어렵다.

3. 2019, 대화상자 리소스 에디터 뻗음

Visual Studio IDE는 2012~2013 즈음부터 외형이 크게 바뀌지 않기 시작했기 때문에 특히 2015와 2017은 내 경험상 거의 분간이 안 된다. 영문판은 웬일로 FILE EDIT 등 메뉴 이름을 잠깐 몽땅 대문자로 표기하는 객기(?)를 부리기 시작했다가 후대 버전에서 객기를 접은 듯하다.
2019는 프로그램의 제목 표시줄이 없어지고 화면 첫 줄에 곧바로 메뉴가 표시되기 시작했다. 현재 열려 있는 솔루션의 이름은 메뉴의 오른쪽에 표시된다. 윕 브라우저들도 그렇고 요즘은 제목 표시줄을 없애는 게 유행이기라도 한가 보다. 게다가 쟤들은 메뉴조차 없애 버리고 Alt키를 눌렀을 때만 메뉴가 표시되게 해 놨다.

그렇게 프로그램의 외형이 야금야금 바뀌는 것이야 좋다고 치는데.. 왜 예전에는 경험한 적이 없던 버그까지 야금야금 끼어 들어가나 모르겠다.
우선 아주 불규칙하지만 분명한 빈도로.. 텍스트 에디터의 폰트가 본인이 수동으로 변경하기 전의 원래 폰트로 되돌아간다. 정확한 재연 조건은 모르겠다. Visual Studio를 열어 놓은 채로 며칠 간격으로 절전 모드에 들어갔다가 복구하기를 반복하다 보면 되돌아가 버린다.

그리고 C++ win32 리소스 중에서 대화상자 편집기만 제대로 안 열리고 프로그램이 무한 루프에 빠지며(= CPU 소모하면서) 응답이 멎는 문제가 있다.
잘 알다시피 Visual Studio 2012부터는 msi 파일을 생성하는 배포 패키지 프로젝트가 짤려서 기본 제공되지 않는다. 별도의 extension을 설치해야만 다시 지원된다. 본인은 회사에서는 그렇게 했다.

그런데 그 extension을 설치한 뒤부터 win32 프로젝트에서 대화상자 편집기가 열리지 않고 IDE가 얼어붙어 버렸다. 그래서 대화상자 리소스를 편집하는 작업을 할 수가 없어졌다.
뒤늦게 그 extension을 disable시키거나 아예 제거해도.. 버전 16.2.3 최신 업데이트를 적용해도, 심지어 Visual Studio를 재설치(복구)해도 그 문제는 해결되지 않았다! 이 VS 2019는 대화상자 리소스를 영원히 편집할 수 없는 절름발이 상태가 된 것 같다.

검색을 해 보니 이 문제는 VS 2019 초창기 시절부터 종종 보고되곤 했던 것 같다. 하지만 release candidate 수준의 옛날 일이지 최신 업데이트에 이르기까지 문제가 발생하거나 해결됐다는 얘기는 딱히 발견하지 못했다.
이러니 Visual Studio는 최신 버전이 구버전의 용도를 완전히 흡수· 대체하지 못하고 구버전도 여전히 병행해서 사용돼야만 할 것 같다. 결국 회사에서도 2010을 따로 설치해야 했다.

4. 2010, 동작은 하지만 이상한 경고 메시지

그럼 구버전은 아무 이상이 없느냐 하면 불행히도 그것도 아니다.
Windows 10 초창기에는 안 그랬던 것 같은데.. 운영체제 업데이트를 몇 번 거치고 나니 VS 2010 devenv.exe는 정체를 알 수 없는 이상한 에러 메시지를 한번 내뱉은 뒤에 실행된다.

The file C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Vsa.tlb could not be loaded. An attempt to repair this condition failed because the file could not be found.
Please reinstall this program.


이미 알려진 문제이며 .NET Framework 3.5를 설치한 뒤에 Visual Studio도 복구(재설치)하면 이런 메시지가 없어질 거라고 하는데..
프로그램 사용을 못 할 정도의 치명적인 오류는 아니니 귀찮아서 안 하고 지낸다. 어차피 VS 2010을 C# 같은 .NET 플랫폼 개발용으로 사용하는 건 아니니 말이다.

5. 컴파일러의 버그

하루는 32비트 정수와 16비트 정수를 인자로 받아서 이걸 한데 뭉친 64비트 정수를 되돌리는 정말 간단한 인라인 함수를 구현한 적이 있었다. 이렇게 생성된 값을 저장하고 불러오게 했는데.. 문제가 발생했다. 불러온 결과가 이전에 저장했던 결과와 일치하지 않고 프로그램이 제대로 동작하질 않았다.

곳곳에다 변수값을 화면에다 찍어 봐도 내가 짠 코드에는 좀체 문제가 없는 것 같고.. 듣도 보도 못한 이상한 값은 전혀 예상치 못했던 곳에서 갑자기 생기고 있었다.
비유하자면 MAKELONG(16012, 76)의 계산 결과값이 저장할 때와 불러올 때가 서로 다르다는 게 믿어지시는가? high word 쪽의 값이 내가 지정한 값이 아니라 32766 같은 엉뚱한 값을 기준으로 계산되었다.

해당 함수를 #pragma를 줘서 최적화를 끄고, 인라이닝을 해제하는 등 별짓을 해도 계산값이 교정되지 않았다. 컴파일러가 구형인 것도 전혀 아니고, 갓 업데이트 받았던 따끈한 Visual C++ 2019 16.3.2였다.
신기한 것은.. { return X|(Y<<32); } 대신

{
    auto ret = X|(Y<<32);
    TRACE("%d %d\n", X,Y);
    return ret;
}

이렇게 함수 인자를 강제로 화면에다 찍게 하면 버그가 발생하지 않고 계산이 맞게 되었다는 것이다.
하지만 저렇게 하지 않고 함수를 아예 #define 매크로 형태로 고쳐도 문제가 동일하게 발생하니.. 이 정도면 변수를 참조하는 코드 자체가 단단히 잘못 생성되고 있는 것이나 마찬가지였다.

수 년 전엔 bit rotation을 구현한 암호화 알고리즘에서도 release와 debug의 동작이 다르고 최적화 적용 여부에 따라 동작이 달라지는 현상을 발견하긴 했는데.. 이 문제는 그것보다도 더 심각한 문제였다.
물론 비트 연산이라는 공통점은 있다. 컴파일러가 << >> | 같은 연산자를 다루는 데서 무리하게 최적화를 시도하는가 보다.

결국 이 버그는 memcpy라는 무식하기 짝이 없는 물건을 동원함으로써 겨우 회피할 수 있었다. 64비트 정수에다가 일단 32비트 값을 대입한 뒤, 4바이트 오프셋에다가 16비트 정수를 강제로 복사하게 했다. 컴파일러가 memcpy는 어째 제멋대로 최적화를 안 했는지 이렇게 하니 프로그램이 깔끔하게 돌아가기 시작했다. 비트 엔디언 독립성은 물론 포기했다.

memcpy는 예전에 align이 맞지 않는 임의의 단위로 메모리를 읽고 써야 할 때.. x86 계열에서는 아무 문제 없다가 ARM 같은 CPU에서 멀쩡한 프로그램이 뻗을 때도 유용하고 사용한 적이 있다.. CPU 특성이나 컴파일러의 특성을 가리지 않고 제일 무식하고 확실하게 메모리를 읽고 쓰는 게 보장돼야 할 때 최후의 보루 역할을 하는 듯하다.
그나저나 컴파일러의 버그임이 명백한 이 현상은 도대체 왜 발생하는지, 해결할 방법이 없나 궁금하다.

이상이다.
본인은 예나 지금이나 개인용 컴터에는 VS 2003, 2010, 2019를 나란히 설치해 놓고 지낸다. 즉, 최신 버전 말고도 2003과 2010은 고정 설치라는 뜻이다.

한때는 최신 API에 대한 설명 때문에 201x의 도움말을 하드에 설치해 놓았으나, 요즘은 마소에서 로컬 도움말은 2015 이후로 업데이트도 안 하고 거의 버린 자식 취급하길래..
그건 포기하고 그냥 옛날 200x 시절의 MSDN을 고전 Windows API 및 기본 C/C++ 레퍼런스용으로 사용한다. 이걸로 충당이 안 되는 최신 정보는 인터넷 조회로 해결하고 말이다.

Visual C++ 201x 버전들에서 본인의 기억에 남아 있는 인상적인 변화 사항은 다음과 같다.

  • 2012: 흰 스킨 도입. Windows XP 타겟 지원을 최초로 중단했다가 별도의 툴킷으로 따로 제공 시작. Syntax coloring이 더 세분화됨. 정적 분석 기능 도입. 예전 같은 서비스 팩 대신, 업데이트 n 형태로 수시로 업데이트 되기 시작
  • 2013: 약간 푸르스름하면서 흰 스킨 도입. 코드 자동 서식 적용 시작, 커뮤니티 에디션 도입.
  • 2015: C 런타임 라이브러리 구조가 개편됨
  • 2017: 설치/업데이트 체계가 전면 개편됨. 안드로이드 등 별별 환경 개발까지 다 지원하기 시작. 오프라인 도움말 앱을 사실상 지원 중단
  • 2019: 프로그램 제목 표시줄이 없어짐. 스플래시 화면이 더 간지나게 바뀜. 색깔이 채도가 약간 더 올라가고 산뜻해짐. 처음 실행했을 때나 기존 솔루션을 닫은 직후에 통상적인 시작 페이지 대신, "원하는 작업을 선택하세요" 대화상자가 표시됨.

Posted by 사무엘

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

본인은 최근에 직장일 때문에 이메일을 자동으로 생성해서 보내는 프로그램이란 걸 난생 처음으로 작성해 봤다. 소켓 API만 써서 말이다.
서식이고 첨부고 몽땅 다 생략한 최소한의 형태만 생각한다면, 이메일을 보내는 것 자체는 내 예상보다 굉장히 간편하게 쉽게 자동화할 수 있는 일이라는 걸 알 수 있었다. SMTP 서버 명령어 몇 개만 스펙대로 주고 받으면 된다.

발신자는 말할 것도 없고 수신자조차도 실제로 수신하는 대상과 화면에 표시되는 수신자가 서로 다르게 얼마든지 조작 가능하다. 스팸 메일을 대량으로 살포하는 건 일도 아니겠다는 걸 이제야 느꼈다. 이런 문제도 있고, 또 이메일 내용을 다른 해커가 가로챌 수도 있으니 이 바닥에도 온갖 복잡한 인증과 암호화 계층이 나중에 도입된 거지 싶다.

이메일과 관련하여 서버에다 요청을 보낼 때는 줄 바꿈 문자가 \n이 아니라 반드시 \r\n을 써야 한다는 게 인상적이었다. 이건 어째 유닉스가 아닌 DOS/Windows 진영의 관행과 일치한다. 그리고 메일 본문의 끝을 의미하는 게 도스의 copy con이 사용하는 Ctrl+Z 같은 제어 문자가 아니라 그냥 "빈 줄+마침표+빈 줄"이다.

또 주목할 만한 것은 DATA(본문)에 들어가는 발신 날짜였다. 난 메일을 보내면 발신 시각 정도는 메일 서버가 자기 시각을 기준으로 당연히 자동으로 넣어 줄 거라고 생각했다. 사람이 이메일을 보낼 때 발신 시각을 일일이 써 넣지 않듯이 말이다.
하지만 내부적으로는 그렇지 않았다. 보내는 쪽에서 알려 줘야 하며, 허위로 조작된 임의의 날짜· 시각을 보내는 것도 얼마든지 가능하다.

그리고 여기에 써 주는 날짜· 시각은 "Tue, 18 Nov 2014 13:57:11 +0000" 같은 형태로, 날짜와 시각, time zone을 모두 포함하고 있다. 심지어 요일이라는 일종의 잉여 정보도 있다.
이 형식은 RFC 2822에 표준 규격으로 정해졌는데, 보다시피 사람이 읽기 편하라고 만들어졌지 컴퓨터의 입장에서 간편하게 읽고 쓸 수 있는 형식은 아니다. 컴퓨터의 관점에서는 그냥 1970년 1월 1일 이래로 경과한 초수, 일명 Unix epoch 숫자 하나가 훨씬 편할 텐데 말이다. time zone도 무시하고 UTC만 통용시키고 말이다.

실제로 이 날짜· 시각 문자열은 그 형태 그대로 쓰이지 않는다. 어차피 이메일 클라이언트가 파싱을 해서 내부적으로 Unix epoch 같은 단순한 형태로 바꿔야 한다. 그래야 당장 메일들을 오래된 것-새것 같은 순서대로 정렬해서 목록을 뽑을 수 있을 테니 말이다. 또한 그걸 출력할 때는 "2014년 11월 18일 오후 10시 57분 11초"처럼 사용자의 언어· 로케일과 설정대로 형태가 또 바뀌게 된다.

그러니 사람보다는 기계가 더 활용하는 날짜 시각 문자열 포맷이 왜 저렇게 복잡한 형태로 정해진 건지 의문이 들지 않을 수 없다. 읽고 쓰기 위해서 달 이름과 요일 이름 테이블까지 참조해야 하고 말이다. 글쎄, SMTP 명령어를 사람이 직접 입력해서 이메일을 보내던 엄청난 옛날에 사람이 읽고 쓰기 편하라고 저런 형태가 정해진 건지는 모르겠다.

Windows API의 GetDateFormat/GetTimeFormat 내지 C 언어의 asctime/ctime 함수 어느 것도 이메일의 날짜· 시각 포맷과 완전히 일치하는 문자열을 되돌리지는 않는다. 특히 C 함수의 경우,

Tue Nov 18 13:57:11 2014

로, 년월일 시분초 요일까지 정보가 동일하고 맨 처음에 요일이 나오는 것까지도 일치하지만.. 이메일 포맷과는 일치하지 않는다. C 함수도 나열 순서와 글자수가 언제나 동일하고 불변인 것이 보장돼 있기 때문에 저걸 변경할 수는 없다. 저 결과값을 그대로 쓸 수도 없으니 답답하기 그지없다.

참고로 일반인이 저런 날짜· 시각 format 함수를 작성한다면 그냥 단순무식하게 sprintf "%02d %s" 같은 방식으로 코딩을 하겠지만, 프로그래밍 언어 라이브러리에서는 그런 짓은 하지 않고 성능을 최대한 중요시하여 각 항목들을 써 넣는 것을 한땀 한땀 직접 구현한다. 해당 라이브러리의 소스를 보면 이를 확인할 수 있다.

Windows API에는 SYSTEMTIME이라는 구조체가 있고, C에는 tm이라는 구조체가 있어서 날짜와 시각을 담는 역할을 한다. 그런데 tm이라니.. 구조체 이름을 무슨 변수 이름처럼 참 이례적으로 짧고 성의없게 지은 것 같다. -_-
C 시절에는 앞에 struct라는 표식을 반드시 덧붙이기라도 해야 했지만 C++은 그런 것도 없으니 더욱 난감하다. C++의 등장까지 염두에 뒀다면 이름을 절대로 저렇게 지을 수 없었을 것이다.

또한 tm 구조체의 멤버들 중 월(tm_mon)은 1이 아니라 0부터 시작한다는 것, 그리고 연도(tm_year)는 실제 연도에서 1900을 뺀 값을 되돌린다는 것도 직관적이지 못해서 번거롭다. 즉, 2019년 7월은 각각 119, 6이라고 기재된다는 것이다. 그에 반해 Windows의 SYSTEMTIME은 그렇지 않으며 wYear과 wMonth에 실제값이 그대로 들어가 있다.

월이 0부터 시작하는 건 쟤네들 문화권에서는 어차피 월을 숫자가 아닌 이름으로 취급하기 때문에 배열 참조의 편의를 위해서 그렇게 했을 것이다. 그런데 연도는.. 무슨 공간을 아끼려고 굳이 1900을 뺐는지 모르겠다. 16비트 int 기준으로 서기 32767년만 해도 정말 까마득하게 먼 미래인걸 말이다.

아 하긴, 20세기 중후반엔 연도의 마지막 두 자리(10과 1)만 써도 70이니 90이니 하면서 월과 일의 숫자 범위보다 월등히 컸기 때문에 변별력이 있었다. 연도도 두 자리만 쓰는 게 관행이었기 때문에 1900을 빼는 것은 그런 관행을 반영한 조치였을 것이다. Office 97은 있어도 Office 07은 없고 2007이니까 말이다.

엑셀 같은 스프레트시트들도 날짜 겸 시각을 저장하는 자료형의 하한이 1900년 1월 1일로 잡혀 있다. 그래서 한국 최초의 철도가 개통한 1899년 9월 18일 같은 날짜는 아슬아슬하게 날짜형으로 저장하지 못하며, 일반 문자열로만 취급된다. =_=;;

이렇게 인간 가독형 날짜· 시각 말고 기계 가독형으로 직렬화된 날짜· 시각을 저장하는 정수 자료형으로 Windows에는 FILETIME이 있고, C에는 time_t가 있다. FILETIME은 Windows NT 시절부터 64비트로 시작했지만 후자는 2000년대 이후에 와서야 2038년 버그를 미연에 방지하기 위해 각 플랫폼별로 64비트로 확장됐다. 사실, 이때부터 PC도 64비트로 바뀌어서 플랫폼에 따라서는 long 같은 자료형도 64비트로 바뀌기도 했다..

그리고 요일이야.. 두 구조체 모두 일요일이 0이고 토요일이 6이다.
요일도 이름이 아닌 숫자로만 취급하는 언어는 내가 알기로 중국어밖에 없는데 혹시 더 있는지 궁금하다. 물론 인간의 언어에는 설마 0요일이 있을 리는 없고 1부터 시작할 텐데.. 중국어의 경우 일요일은 日이어서 이게 0 역할을 하고, 월요일부터 토요일까지가 1요일~6요일에 대응한다.

이상이다. 이메일 얘기로 시작해서 날짜 시각 얘기로 소재가 바뀌었는데..
이메일(POP3/SMTP)을 비롯해 HTTP, FTP 같은 표준 인터넷 프로토콜들을 클라이언트와 서버를 모두 소켓 API만으로 직접 구현해 보는 건 코딩 실력의 향상에 도움이 될 것 같다. 일상생활에서야 이미 만들어져 있는 솔루션만 사용하면 되니까 실용적인 의미는 별로 없겠지만, 학교에서 학술..도 아니고 그냥 교육적인 의미는 충분히 있을 테니 말이다. 내부 구조를 직접 살펴보면, 이런 프로토콜의 secure 버전이 왜 따로 만들어져야 했는지 이유도 알게 될 것이다.

더구나 이를 응용해서 특정 메일에 대해 자동 회신을 보내는 프로그램, 한 FTP 서버의 파일을 다른 FTP 서버로 올리는 프로그램까지 만드는 건 시험이나 과제 용도로도 괜찮아 보인다. 요즘은 FTP 같은 거 명령어로 이용할 줄 아는 사람이 얼마나 될까? 당장 본인도 모른다. ㅎㅎ

이메일을 쓰지 않는다는 도널드 커누쓰 할배가 문득 생각난다. 이분이야 뭐 1970년대, 컴퓨터 네트워크라는 건 그냥 기업· 연구소, 정부 기관만의 전유물로 여겨졌고 이메일이란 게 처음으로 발명됐던 시절에 그걸 다뤄 왔던 분이다. 그러다가 현역 은퇴 후에 때려치우고 온라인 공간의 속세와 단절한 셈이다. 이젠 뭐가 아쉬워서 누구에게 이메일로 연락을 하거나 연락을 기다려야 할 처지도 전혀 아니고.. 그냥 아날로그 종이 편지를 취급하는 것만으로 충분하시댄다.

Posted by 사무엘

2019/12/18 08:32 2019/12/18 08:32
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1695

C/C++은 어떤 명칭에 대해 선언과 정의의 구분이 명확한 축에 드는 언어이다. 정의는 선언도 같이 포함하지만 그 역은 성립하지 않는다. 전자는 심판의 선고이고, 후자는 집행이라고 봐도 되겠다.

(1) 함수: 실행되는 코드를 담고 있기 때문에 {}에 둘러싸여 정의된 몸체의 존재감이 압도적인 물건이다. 또한 함수의 선언부는 자신의 프로토타입(인자의 개수와 타입, 리턴값의 타입)을 나타내기도 한다. 그렇기 때문에 얘는 소형 인라인 형태가 아닌 이상, 선언과 정의의 구분이 가장 명확하다.

(2) 자료형: 구조체나 클래스는 함수보다야 선언 따로 정의 따로일 일이 훨씬 드물다. 하지만 헤더에서는 포인터 형태만 사용하는데 쓸데없는 #include 의존성을 또 만들지 않기 위해 class Foo; 같은 불완전한 타입을 선언만 하는 게 가능은 하다. 마치 함수 선언처럼 말이다.
선언만 존재하는 불완전한 타입은 sizeof 연산자를 적용할 수 없으며, 포인터형의 경우 *나 ->로 역참조해서 사용할 수도 없다.

(3) static 멤버/전역 변수: 변수는 선언하는 것 자체 말고 딱히 {}로 둘러싸인 세부 정의가 존재하지 않는다. 생성자 인자라든가 초기화 값(initializer)이 쓰이긴 하지만 그건 definition, body와는 성격이 완전히 다른 정보이니 말이다.

다만, 지역 변수 말고 클래스의 static 멤버에 대해서는 static int bar와 int Foo::bar 같은 선언/정의 구분이 존재한다. 그리고 전역 변수도 extern이라고 선언된 놈은 정의가 아닌 선언 껍데기일 뿐이다. (실제 definition은 다른 translation unit에 존재한다는..)
사실, global scope에서 함수의 선언도 앞에 extern이 생략된 것이나 마찬가지이다. 지역 변수의 선언들이 모두 구 용법의 auto가 생략된 형태인 것처럼 말이다.

함수건 변수건 선언은 여러 군데에서 반복해서 할 수 있지만 몸체 정의는 딱 한 군데에만 존재한다. 이는 마치 분향소와 빈소의 관계와도 비슷해 보인다.
이런 선언부에서는 배열의 경우 그 구체적인 크기를 생략할 수 있다. * 대신 []을 써서 얘는 정확한 크기는 모르지만 어쨌든 포인터가 아닌 배열이라고 막연하게 선언할 수 있다는 것이다. 그리고 const 변수는 초기화 값이 선택이 아니라 필수인데, 이 역시 선언 단계에서 생략될 수 있다.

1. 함수와 구조체: 상호 참조를 위한 불완전한 전방 선언

(1) 함수나 (2) 구조체/클래스는 상호 참조를 할 수 있다. A라는 함수에서 B를 호출하고, B도 A를 호출할 수 있다. 또한, X라는 구조체에서 Y라는 구조체의 포인터를 멤버로 갖는데, Y도 내부적으로 X의 포인터를 갖고 있을 수 있다.

요즘 프로그래밍 언어들은 구조적으로 같은 소스 코드를 두 번 읽어서 파싱하게 돼 있기 때문에 한 함수에서 나중에 등장하는 다른 함수를 아무 제약 없이 참조할 수 있다. C++도 그런 요소가 있기 때문에 한 클래스의 인라인 멤버 함수에서 클래스 몸체의 뒷부분에 선언된 명칭에 곧장 접근할 수 있다. 즉, 다음과 같은 코드는 컴파일 된다.

class Foo {
public:
    void func1() {
        func2();
    }
    void func2() {
        func1();
    }
};

하지만 global scope에서 이런 코드는 적어도 C++ 문법에서는 허용되지 않는다.

void Global_Func1() {
    Global_Func2();
}
void Global_Func2() {
    Global_Func1();
}

맨 앞줄에 void Global_Func2(); 이라고 Global_Func2라는 명칭이 껍데기만이라도 forward(전방) 선언돼 있어야 한다. 파스칼 언어에는 이런 용도로 아예 forward라는 지정자 키워드가 있기도 하다.
매우 흥미로운 것은..

struct DATA1 {
    DATA2* ptr;
};
struct DATA2 {
    DATA1* ptr;
};

이렇게 구조체끼리 상호 참조를 하기 위해서는..
심지어 클래스 안의 구조체라 하더라도 앞에 struct DATA2는 반드시 미리 전방 선언이 돼 있어야 한다는 것이다. 클래스 안에 선언된 멤버 함수와는 취급이 다르다. 왜 그런 걸까? 멤버 함수의 몸체는 클래스 밖에 완전히 따로 정의될 수도 있지만 구조체의 몸체는 그럴 수 없다는 차이 때문인 듯하다.

원래 파스칼과 C는 옛날에 컴파일러의 구현 난이도와 동작 요구 사양을 낮추기 위해, 소스 코드를 한 번만 읽으면서 곧장 parsing이 가능하게 설계되기도 했다. 모든 명칭들은 사용되기 "전에" 정의까지는 아니어도 적어도 선언은 미리 돼 있어야 컴파일 가능하다. 아무 데서나 '정의'만 한번 해 놓으면 아무 데서나 그 명칭을 사용할 수 있는 그런 자유로운 언어가 아니라는 것이다.

함수와 전역 변수의 경우, 그 다음으로 몸체 정의를 찾아서 실제로 '연결'하는 건 잘 알다시피 링커가 할 일이다. 단지, 구조체/클래스는 몸체가 당장 컴파일 과정에서 그때 그때 쓰이기 때문에(멤버의 타입과 오프셋...) 링크가 아닌 컴파일 단계에서 실제 몸체를 알아야 한다는 차이가 있다.

불완전한 타입에 대해서 거기에 소속된 구조체/클래스를 불완전한 형태로 또 중첩 선언하는 것은 가능하지 않다.

class A;
class A::B;

A의 몸체를 모르는 상태에서 연쇄적으로 B를 저렇게 또 선언할 수는 없다는 것이다. 그걸 허용하는 건 C++을 동적 타입 언어급으로 만드는 너무 사악한(?) 짓이 될 것 같다. 특히 이미 자유도가 너무 높은 템플릿을 구현하는 것까지 생각했을 때 말이다.
실체가 없는 저런 자료형의 포인터를 무리하게 만들 바에야 아예 void* 포인터를 그때 그때 캐스팅해서 쓰고 말겠다. 아니면, 저런 식으로 다단계 scope 구분만 하는 게 목적이라면 클래스 대신 namespace라는 훌륭한 대체제가 있다.

2. 구조체: 전방 선언과 다중 상속 사이의 난감함

이렇게 몸체를 모르는 클래스를 불완전 전방 선언만 해서 쓰는 것은 일면 편리하지만.. C++이 제공하는 다른 기능 내지 이념과 충돌해서 난감한 상황을 만들 때도 있다.
즉, class X와 class Y라고 이름밖에 모르던 시절에는 X와 Y는 서로 완전히 남남이며, 포인터 형변환도 오프셋 보정 없이 단순무식한 C-style로만 하면 된다.

그런데 알고 보니 X와 Y가 다중 상속으로 얽힌 사이라면.. 몸체를 모르던 시절과 알고 난 뒤의 컴파일러의 코드 생성 방식이 서로 달라질 수밖에 없다. 특히 X 내지 Y의 멤버 함수를 가리키는 pointer-to-member 타입의 크기와 구현 방식도 달라지게 된다. X가 전방 선언만 돼서 아무런 단서가 없을 때가 제일 복잡하고 까다롭다.

Visual C++의 경우, 얘가 전방 선언만 됐지만 다중/가상 상속 같은 것 안 쓰는 제일 단순한 형태이기 때문에 pointer-to-member도 제일 단순한 형태로만 구현해도 된다고 단서를 제공하는 비표준 확장 키워드를 자체적으로 제공할 정도이다. 그만큼 C++의 스펙은 복잡 난해하고 패러다임이 서로 충돌하는 면모도 존재한다.

이렇듯, 명칭의 선언과 정의라는 간단한 개념을 고찰함으로써, C/C++ 이후의 언어들은 선배 언어의 복잡 난해함을 어떻게든 감추고 사용자와 컴파일러 개발자의 입장에서 다루기 편한 언어를 만들려고 어떤 개량을 했는지를 알 수 있다. 당장 Java만 해도 헤더/소스 구분 없이 한 클래스에서 각종 함수나 명칭을 수정하면 번거로운 재컴파일 없이도 그걸 다른 소스 코드에서 곧장 사용 가능하니 얼마나 편리한가 말이다.

3. 함수: extern "C"

앞서 살펴본 바와 같이 extern은 static library 형태이든 DLL/so 방식이든 무엇이건, 외부로 노출되는 전역 함수 및 변수 명칭을 선언하는 키워드이다. 그런데 기왕 대외 선언을 하는 김에 노출을 하는 방식도 옵션을 줘서 같이 지정할 수 있다.
모르는 사람에게는 굉장히 기괴해 보이는 문법인 extern "C"가 바로 그것이다. 이건 함수 명칭을 C++ 스타일로 decorate를 할지, 아니면 예전의 C 시절처럼 원래 이름을 변조 없이 그대로 선언할지를 지정한다.

C++에서 변조니 decorate니 해서 굳이 언어의 ABI 차원에서 호환을 깨뜨려야 하는 이유는.. C++에는 C와 달리 함수 인자를 기반으로 오버로딩이 존재하기 때문이다. 그러니 argument의 개수와 타입들에 대한 정보가 이름에 첨가돼야만 이 함수를 그 이름으로 유일하게 식별할 수 있다.

뭐, static 함수는 대외 노출이 아니니 한 번역 단위 안에서 함수 이름이야 어떻게 붙이건 전혀 상관없으며.. C++ 클래스 멤버 함수는 애초에 C언어에서 접근 불가능한 물건이이고 무조건 C++ 방식으로 decoration을 해야 한다. 그러니 extern "C" 옵션이 필요한 곳은 C와 C++이 모두 접근 가능한 일반 전역 함수 정도로 한정된다.

"C" 말고 쓸 수 있는 문자열 리터럴은 "C++".. 요 둘뿐이다. 그리고 "C++"은 디폴트 옵션이므로 signed만큼이나 잉여이고, 오늘날까지도 사실상 "C"만 쓰인다.
만들고 있는 라이브러리가 자기 제품 내부에서밖에 안 쓰이거나, 어차피 소스째로 통째로 배포되는 오픈소스여서 특정 컴파일러의 ABI에 종속되어도 아무 상관 없다면.. 함수를 C++ 형태로, 아니 C++ 클래스 라이브러리 형태로 선뜻 만들 수 있을 것이다.

그게 아니라면 외부 노출 함수 이름은 어느 언어에서나 쉽게 import 가능한 extern "C" 형태로 만드는 게 일반적이다. extern "C" 다음에는 이 구간에서 선언되는 명칭들을 모두 C 방식으로 노출하라고 중괄호 {}까지 줄 수 있으니 생소함과 기괴함이 더해진다.

이건 컴파일러의 구문 분석 방식을 변경하는 옵션이 아니다. {} 안의 코드는 C 문법으로만 해석하라는 말이 절대 아니다. extern "C" 방식으로 선언된 함수의 안에서도 템플릿, 지역 클래스 등 C++ 문법은 얼마든지 사용할 수 있고 타 C++ 객체를 참조할 수 있다. 단지 이 함수는 동일 명칭의 여러 오버로딩 버전을 만들어서 대외적으로 제공할 수 없을 뿐이다.

또한, 컴파일러의 최적화나 코드 생성 방식에 영향을 주지도 않는다. stdcall, pascal, cdecl 같은 calling convention이야 인자를 스택에다 올리는 순서 내지 스택 주소 복귀를 하는 주체(caller or callee)를 지정하는 것이니까 코드 생성 방식에 영향을 준다. 언어 문법 차원에서의 프로토타입이 동일하더라도 calling convention이 다른 함수끼리는 포인터가 서로 호환되지 않는다.
그에 반해 extern "C" 지정이 잘못되면 obj와 lib 사이에 공급된 명칭과 요청한 명칭이 일치하지 않아서 끽해야 링크 에러가 날 뿐이다. 개념이 이렇게 정리된다.

Posted by 사무엘

2019/11/16 08:32 2019/11/16 08:32
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1684

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/03   »
          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:
2633861
Today:
659
Yesterday:
1754