동일한 내용의 코드를 세가지 버전으로 작성해내는 프로그래머의 모범적 자세... 라고 표현하면 나름 위로가 되기도 하겠지만, 최종 코드를 만들고 나니 이미 숱하게 보았던, 하지만 언제나 쉽사리 지나쳤던 바로 그 루틴이었다는데서 느끼는 허탈감.

배경
삽질 1의 그것과 동일하다. out of process에서 동작하는 COM Server와 통신하는 COM Client. 인스턴스로 올라간 COM Server에 client가 달라붙기 위해서 Running Object Table에 Server를 등록하고, client는 이 테이블에서 해당 Server를 찾아 연결하기까지의 내용. 알고보면 IPC(Inter Process Communication)를 이루는 가장 쉬운 방법(동기화 문제는 물론이요, 개체 기반 프로그래밍 패러다임까지 그대로 보존한 채 이를 이루기에)이 되겠다.

첫 번째 버전
위 시나리오를 그대로 따른 코드다. ROT와 이에 따른 Moniker에 대한 개념만 갖고 있다면 대강 reference 짜깁기해서 얻어낼 수 있는 코드이겠다.

서버측 코드
  1 ...
2 CComPtr<IMoniker> spMoniker;
3 HRESULT hr = ::CreateClassMoniker(CLSID_Communicator, &spMoniker);
4
5 CComPtr<IRunningObjectTable> spRot;
6 hr = GetRunningObjectTable(0, &spRot);
7 if(FAILED(hr)) { return hr; }
8
9 hr = spRot->Register(ROTFLAGS_REGISTRATIONKEEPSALIVE,
10 dynamic_cast<ICommunicator*>(this),
11 spMoniker,
12 &rotRegisterVal_);
13 ...
CLSID_Communicator는 서버측 클래스 개체의 클래스 GUID이고 ICommunicator는 서버가 노출할 인터페이스이다. 클래스 모니커를 하나 만들어 서버 인스턴스(this: 서버 자신)를 ROT에 등록하는 모습이 되겠다.

클라이언트측 코드
  1 CComPtr<ICommunicator>    spCommunicator_;
2 ...
3 CComPtr<IRunningObjectTable> spTable;
4 CComPtr<IEnumMoniker> spEnumMoniker;
5 CComPtr<IMoniker> spMoniker;
6
7 HRESULT hr = E_FAIL;
8 if(GetRunningObjectTable(0, &spTable) != S_OK) { return hr; }
9
10 spTable->EnumRunning(&spEnumMoniker);
11 spEnumMoniker->Reset();
12
13 CComPtr<IUnknown> spUnk;
14
15 while(spEnumMoniker->Next(1, &spMoniker, NULL) == S_OK)
16 {
17 hr = spTable->GetObject(spMoniker, &spUnk);
18 spMoniker.Release();
19
20 if(FAILED(hr))
21 {
22 if(spUnk) { spUnk.Release(); }
23 continue;
24 }
25
26 hr = spUnk.QueryInterface(&spCommunicator_);
27 spUnk.Release();
28
29 if(FAILED(hr))
30 {
31 if(spCommunicator_) { spCommunicator_.Release(); }
32 continue;
33 }
34 else { break; }
35 }
상당히 길다. 하지만 알고보면 단순해서 위 기본 시나리오에서 달라질 게 없다. ROT에 등록된 개체를 하나씩 조사하여 ICommunicator를 구현한 개체를 찾는 것 뿐이다.

두 번째 버전/클라이언트 코드
헌데, '이야, 내 생각대로 구현한게 잘 돌아가네?' 하며 잘 쓰고 있다가 불연듯 코드가 너저분하다는 생각이 드는거다. 특히나 client쪽에서. 편리한거 좋아하는 MS에서 이런 흔한 시나리오를 위한 utility 함수를 안만들었을리 없지, 생각하며 웹을 뒤져보니 BindMoniker()란 함수가 눈에 띈다. 특정 모니커와 바인딩을 한다... 위 클라이언트 코드가 하는 일과 맞아떨어지는 듯한. 아니나 다를까, 다음과 같이 확 줄어든 코드가 가능해진다.
  1 ...
2 CComPtr<IMoniker> spMoniker;
3 HRESULT hr = ::CreateClassMoniker(CLSID_Communicator, &spMoniker);
4 if(FAILED(hr)) { return hr; }
5
6 CComPtr<IUnknown> spUnk;
7 hr = ::BindMoniker(spMoniker,
8 0,
9 IID_ICommunicator,
10 (LPVOID*)&spCommunicator_);
11 spMoniker.Release();
12 ...
확연히 줄어든 코드. 게다가 서버측에서 만들었던 클래스 모니커 코드를 그대로 사용하기 때문에 의미론 상, 코드 가독성을 놓고 보아도 훨 뛰어나다. 아이, 좋아라~

위치 투명성 : location transparency
하지만 COM하면 위치 투명성, 즉 클라이언트에서는 서버가 in process에 있건, out-of process에 있건, remote에 있건 상관안하고 쓸수 있다는 게 자랑 아니던가. 코드가 짧아졌다지만 여전히 이 위치 투명성하고는 거리가 멀다.

그런 이유로 한번 더 웹을 뒤지다 눈에 걸린 함수, CoGetClassObject(). CoCreateInstance()는 서버 인스턴스를 생성하기에 아니겠다 싶어 딴놈을 찾다 눈에 걸린 함수다. 슬쩍보니 CoCreateInstance()랑 매개변수 목록도 똑같네. 어디, 이놈으로 바꿔보자.

최종 버전/클라이언트 코드
  1 ...
2 hr = ::CoGetClassObject(CLSID_Communicator,
3 CLSCTX_LOCAL_SERVER,
4 0,
5 IID_ICommunicator,
6 (LPVOID*)&spCommunicator_);
7 ...
이제는 달랑 한줄이다. 오, 행복해라. 근데 이 함수 어디서 많이 봤던거 같다. 맞다. out of process COM하면 항상 나오던 그 함수였다. 여기까지오니 이제 서버측 코드도 눈에 거슬린다. 또다시 웹을 뒤지니 항시 지나치던 CoRegisterClassObject()가 입질을 하는구만. 윽, 등록(Register)이란 말이 생략한 위치가 바로 ROT였구나. 이제 좀 감이 온다.

최종 버전/서버측 코드
  1 ...
2 return CoRegisterClassObject(CLSID_Communicator,
3 dynamic_cast<ICommunicator*>(this),
4 CLSCTX_LOCAL_SERVER,
5 REGCLS_MULTIPLEUSE,
6 &rotRegisterVal_);
7 ...
당연히 서버 종료시에는 등록 해제를 해야하고, 이를 위해 CoRevokeClassObject()를 사용한다.

허탈하기도 하고, 뿌듯하기도 하고. 내부 동작을 확인했다는데 의미를 둘 수도 있겠지만, 그간 상당히 보아온 코드를 싸그리 잊고 먼길로 돌아서왔다는 것은 움...이건 어떻게 해석해야 할까. 음냐.
2008/03/25 02:33 2008/03/25 02:33

트랙백 주소 :: http://anyflow.net/trackback/366

댓글을 달아 주세요

배경
  1. ROT(Running Object Table)를 통해 IPC(Inter-Process Communication)통신을 이루는 COM Server/Client. 본 채널을 가리켜 lightweight RPC라고 한다나?(Server와 Client 모두 동일 호스트에서 동작하기 때문에 RPC는 아니라고).
  2. COM Server는 USB 드라이브와 통신하며, COM Client는 IE browser에 내장되어 Server와 연결짓는 역할(따라서 COM client 역시 browser를 위한 COM Server 역할을 담당). Client의 메서드와 이벤트는 javascript를 통해 호출 및 수신한다.
  3. COM Server/Client 모두의 threading model은 STA(Single Thread Apartment).
  4. COM Server 쪽에는 UI가 있어 내장 IE Browser가 달려있음.
문제
  1. COM Server 메서드 호출은 성공. 허나 COM Server에서 event firing시 Fire 위치에 따라 성공 또는 실패하는 황당한 상황이 발생. 실패 시 HRESULT 값 : 0x8001010d(An outgoing call cannot be made since the application is dispatching an input-synchronous call)
  2. 우연찮게 COM Server의 threading model을 MTA(Multi Thread Apartment)로 바꿨더니, 1의 문제가 사라짐. 하지만 내장 IE Browser window의 위치가 desktop window origin에 달라붙어 떨어지지 않는 상황이 발생(MoveWindow() 등의 API가 전혀 안먹음).
문제 발생 원인
  • 이벤트 메서드(Fire_XX()) 호출이 실패한 위치는 USB 드라이브 측에서 던진 message(드라이브 연결/끊김 등) handler 내에서였음. 본 handler는 message를 던진 측 routine과 동기적으로 동작하기에, 본 handler 내에서 (process 외부에서 동기적으로 동작할?) method를 호출할 수 없기에 발생함. 위 HRESULT 값을 통해 본 사항을 간접적으로 확인할 수 있겠다.
해결
  • 동기적으로 발생하는 calling chain을 끊어버린다. 즉, USB event handler 내에서 non-blocking 모드로 동작하는 PostMessage()를 던짐으로써 USB 드라이브에서 던진 message에 대해 비동기적으로 이벤트를 처리한다.
    reference : http://discuss.develop.com/archives/wa.exe?A2=ind0303a&L=atl&P=3378
etc.
  1. 왜 MTA 기반에서는 정상적으로 동작했을까? STA 기반의 COM Server에서는 이벤트 발생 메서드인 Fire_XX()가 동기적으로 동작하나(blocking mode), MTA에서는 non-blocking 모드로 동작하기에 동기적 calling chain이 만들어지지 않으므로. 왜 이런식으로 동작하는지는 연구 대상.
    COM threading model reference :
    Understanding and Using COM Threading
    INFO: Descriptions and Workings of OLE Threading Models
  2. MTA 기반에서 IE Browser window의 위치가 제멋대로 노는 원인은 미결. IE Browser Control은 out-of-process COM이라는데, 요게 단서가 될지도 모른다는...
푸념
  • message가 SendMessage(동기 모드)으로 던저졌는지, PostMessage(비동기 모드)로 던저졌는지 그걸 뭔 수로 아는가. 딱히 구분할 값을 던져주는 것도 아니고. 각 message에 대한 설명서를 일일이 확인해야 하는 상황. 나참, 어이가 없어서리.
2008/03/20 18:25 2008/03/20 18:25

트랙백 주소 :: http://anyflow.net/trackback/364

댓글을 달아 주세요

마이크로커널(microkernel)의 정의
- 커널, 즉 모듈의 기능과 OS의 확장을 위한 기반은 core로만 남아야 한다는 설계 철학.

마이크로커널의 특징
- 마이크로 커널 아키텍처의 핵심은 (주소 공간 관리, 스래드 관리, IPC 등의) 가장 핵심적인 OS 기능만을 (커널 모드에서 동작하는) core에 담고 그 외 나머지는 사용자 모드로 넘긴다.
- 서비스라던가 덜 중요한 OS의 기능(device driver, file system, virtual memory manager, windowing system, security service 등)은 서버 프로세스로 구현하되, 이들간 통신은 core를 매개로 하는 메시지 전달(message passing)을 통해 peer 기반에서 이룬다.
- (monolithic 커널과 같은) 기존의 계층적 방식(layered approach)은 수직적 바 방식(vertical bar approach)으로 변경되는데, 이는 계층적 방식이 모듈화를 이루고 있음에도 불구하고 한 계층의 변경이 인접한 타 계층의 변경을 야기하기 때문이다.
- core에 어떤 기능이 포함되어야 한다는 정해진 규칙은 없다. 일반적으로 저수준 메모리 관리, IPC, I/O와 인터럽트 관리 기능이 core에 포함된다.

more..

2007/12/11 06:20 2007/12/11 06:20

트랙백 주소 :: http://anyflow.net/trackback/320

댓글을 달아 주세요