Windows COM(Component Object Model)은 1990년대 Microsoft가 설계한 바이너리 인터페이스 표준이다. OLE, ActiveX, DirectX, Shell Extension 등 Windows의 핵심 기술이 COM 위에 구축되어 있다. 그런데 왜 COM은 일반 wchar_t* 대신 BSTR이라는 별도의 문자열 타입을 만들었을까? 이 글에서는 COM의 메모리 관리 철학과 BSTR, VARIANT의 내부 구조를 분석한다.
1. BSTR: Length-Prefixed Wide String
왜 BSTR인가?
C 스타일 문자열의 근본적인 문제:
| 문제 | C 문자열 | BSTR |
|---|
| 길이 확인 | O(n) - null 탐색 | O(1) - 앞 4바이트 |
| null 포함 | 불가능 | 가능 (바이너리 안전) |
| 크기 정보 | 없음 | 앞에 저장됨 |
| 언어 중립 | C/C++ 전용 | VB, JS, Python 등 연동 |
COM은 언어 중립적 바이너리 인터페이스를 목표로 했다. Visual Basic, JavaScript, Python 등 다양한 언어가 COM 객체를 사용하는데, 각 언어의 문자열 표현이 다르다. BSTR은 이 문제를 해결한다.
메모리 레이아웃
BSTR bstr = SysAllocString(L"Hello");
| 오프셋 | 크기 | 내용 | 값 (예시) |
|---|
| -4 | 4 bytes | 바이트 길이 (null 제외) | 10 (5문자 × 2바이트) |
| 0 | 가변 | 문자열 데이터 (UTF-16LE) | H e l l o |
| +10 | 2 bytes | null terminator | \0\0 |
핵심: BSTR 포인터는 문자열 시작을 가리킨다. 길이는 포인터 - 4 위치에 있다.
BSTR bstr = SysAllocString(L"Hello");
UINT len = *((UINT*)bstr - 1);
UINT len = SysStringLen(bstr);
UINT bytes = SysStringByteLen(bstr);
BSTR vs LPWSTR
둘 다 wchar_t*로 정의되지만, 의미가 다르다:
typedef WCHAR* BSTR;
typedef WCHAR* LPWSTR;
| 특성 | BSTR | LPWSTR |
|---|
| 길이 정보 | 포인터 앞 4바이트에 저장 | 없음 (null로 판단) |
| 할당 | SysAllocString | malloc, new, 스택 등 |
| 해제 | SysFreeString | 할당 방식에 따름 |
| null 허용 | 문자열 중간에 null 가능 | null이 끝을 의미 |
| COM 마샬링 | 자동 지원 | 수동 처리 필요 |
위험한 실수:
BSTR bstr = L"Hello";
SysFreeString(bstr);
BSTR bstr = SysAllocString(L"Hello");
SysFreeString(bstr);
2. BSTR 할당과 해제
SysAllocString 계열 함수
| 함수 | 용도 | 반환값 |
|---|
SysAllocString(LPCOLESTR) | null-terminated 문자열로부터 생성 | BSTR |
SysAllocStringLen(LPCOLESTR, UINT) | 길이 지정, null 포함 가능 | BSTR |
SysAllocStringByteLen(LPCSTR, UINT) | 바이트 배열로부터 생성 | BSTR |
SysReAllocString(BSTR*, LPCOLESTR) | 재할당 | BOOL |
SysFreeString(BSTR) | 해제 | void |
SysStringLen(BSTR) | 문자 수 반환 | UINT |
SysStringByteLen(BSTR) | 바이트 수 반환 | UINT |
내부 구현 추정
BSTR SysAllocStringLen(const OLECHAR* psz, UINT len) {
UINT bytes = len * sizeof(OLECHAR);
BYTE* raw = (BYTE*)CoTaskMemAlloc(4 + bytes + 2);
if (!raw) return NULL;
*((UINT*)raw) = bytes;
BSTR bstr = (BSTR)(raw + 4);
if (psz) {
memcpy(bstr, psz, bytes);
} else {
memset(bstr, 0, bytes);
}
bstr[len] = L'\0';
return bstr;
}
void SysFreeString(BSTR bstr) {
if (bstr) {
BYTE* raw = (BYTE*)bstr - 4;
CoTaskMemFree(raw);
}
}
UINT SysStringLen(BSTR bstr) {
if (!bstr) return 0;
return *((UINT*)bstr - 1) / sizeof(OLECHAR);
}
NULL BSTR의 의미
| 값 | 의미 | SysStringLen 반환 |
|---|
NULL | 빈 문자열 (유효) | 0 |
SysAllocString(L"") | 빈 문자열 (할당됨) | 0 |
SysAllocString(L"Hello") | "Hello" | 5 |
중요: COM에서 NULL BSTR은 빈 문자열로 취급된다. 에러가 아니다.
void ProcessString(BSTR bstr) {
if (bstr && SysStringLen(bstr) > 0) {
}
UINT len = bstr ? SysStringLen(bstr) : 0;
}
왜 별도의 할당자인가?
COM 객체는 프로세스 경계를 넘어 통신한다. DLL A에서 할당한 메모리를 DLL B에서 해제해야 하는 경우가 발생한다:
| 상황 | 문제 | 해결책 |
|---|
DLL A: malloc → DLL B: free | 힙 손상 (각 DLL이 다른 CRT 힙 사용 가능) | 공유 할당자 사용 |
EXE: new → COM 객체: delete | 같은 문제 | CoTaskMemAlloc |
CoTaskMemAlloc vs malloc
| 함수 | 할당자 | 용도 |
|---|
malloc / free | CRT 힙 | 모듈 내부 전용 |
new / delete | CRT 힙 | 모듈 내부 전용 |
HeapAlloc / HeapFree | 프로세스 힙 | 저수준 Windows API |
CoTaskMemAlloc / CoTaskMemFree | COM 힙 | COM 인터페이스 경계 |
SysAllocString / SysFreeString | COM 힙 (BSTR 전용) | BSTR만 |
HRESULT GetName(BSTR* pbstrName) {
*pbstrName = SysAllocString(L"MyObject");
return S_OK;
}
HRESULT GetData(BYTE** ppData, ULONG* pcb) {
*pcb = 100;
*ppData = (BYTE*)CoTaskMemAlloc(*pcb);
return S_OK;
}
IMalloc 인터페이스
COM 할당자의 근본:
IMalloc* pMalloc = NULL;
CoGetMalloc(1, &pMalloc);
void* p = pMalloc->Alloc(100);
pMalloc->Free(p);
pMalloc->Release();
void* p = CoTaskMemAlloc(100);
CoTaskMemFree(p);
4. IUnknown: COM의 근본
세 가지 메서드
모든 COM 인터페이스는 IUnknown을 상속한다:
interface IUnknown {
virtual HRESULT QueryInterface(REFIID riid, void** ppv) = 0;
virtual ULONG AddRef() = 0;
virtual ULONG Release() = 0;
};
| 메서드 | 역할 |
|---|
QueryInterface | 다른 인터페이스 요청 |
AddRef | 참조 카운트 증가 |
Release | 참조 카운트 감소, 0이면 삭제 |
AddRef/Release 구현
class CMyObject : public IUnknown {
LONG m_cRef;
public:
CMyObject() : m_cRef(1) {}
ULONG AddRef() override {
return InterlockedIncrement(&m_cRef);
}
ULONG Release() override {
ULONG cRef = InterlockedDecrement(&m_cRef);
if (cRef == 0) {
delete this;
}
return cRef;
}
HRESULT QueryInterface(REFIID riid, void** ppv) override {
if (riid == IID_IUnknown) {
*ppv = static_cast<IUnknown*>(this);
AddRef();
return S_OK;
}
*ppv = NULL;
return E_NOINTERFACE;
}
};
참조 카운트 규칙
| 규칙 | 설명 |
|---|
| 생성 시 1 | 객체 생성 시 카운트 = 1 |
| 복사 시 AddRef | 포인터 복사하면 AddRef 호출 |
| 소멸 시 Release | 포인터 버리면 Release 호출 |
| out 파라미터 | 호출자가 Release 책임 |
| in 파라미터 | 호출 중에만 유효, AddRef 불필요 |
void UseObject(IUnknown* pUnk) {
pUnk->DoSomething();
}
void StoreObject(IUnknown* pUnk) {
pUnk->AddRef();
m_pStored = pUnk;
}
void ClearStored() {
if (m_pStored) {
m_pStored->Release();
m_pStored = NULL;
}
}
5. VARIANT: 타입 태그 + Union
왜 VARIANT인가?
COM은 언어 중립적이다. C++의 int와 VBScript의 Integer, JavaScript의 Number를 어떻게 통일할까?
struct VARIANT {
VARTYPE vt;
WORD wReserved1;
WORD wReserved2;
WORD wReserved3;
union {
LONGLONG llVal;
LONG lVal;
BYTE bVal;
SHORT iVal;
FLOAT fltVal;
DOUBLE dblVal;
VARIANT_BOOL boolVal;
BSTR bstrVal;
IUnknown* punkVal;
IDispatch* pdispVal;
SAFEARRAY* parray;
};
};
주요 VARTYPE 값
| VARTYPE | 값 | C++ 타입 | 설명 |
|---|
| VT_EMPTY | 0 | - | 초기화 안 됨 |
| VT_NULL | 1 | - | SQL NULL |
| VT_I2 | 2 | short | 16비트 정수 |
| VT_I4 | 3 | long | 32비트 정수 |
| VT_R4 | 4 | float | 32비트 실수 |
| VT_R8 | 5 | double | 64비트 실수 |
| VT_BSTR | 8 | BSTR | 문자열 |
| VT_BOOL | 11 | VARIANT_BOOL | 불리언 (-1=TRUE, 0=FALSE) |
| VT_UNKNOWN | 13 | IUnknown* | COM 인터페이스 |
| VT_DISPATCH | 9 | IDispatch* | Automation 인터페이스 |
| VT_ARRAY | 0x2000 | SAFEARRAY* | 배열 (OR로 조합) |
| VT_BYREF | 0x4000 | - | 참조 (OR로 조합) |
VARIANT 조작 함수
| 함수 | 용도 |
|---|
VariantInit(VARIANT*) | VT_EMPTY로 초기화 |
VariantClear(VARIANT*) | 내용 해제 후 VT_EMPTY |
VariantCopy(VARIANT*, VARIANT*) | 깊은 복사 |
VariantChangeType(VARIANT*, VARIANT*, USHORT, VARTYPE) | 타입 변환 |
VARIANT var;
VariantInit(&var);
var.vt = VT_I4;
var.lVal = 42;
VariantClear(&var);
var.vt = VT_BSTR;
var.bstrVal = SysAllocString(L"Hello");
VariantClear(&var);
VARIANT_BOOL 주의사항
| 값 | 의미 | 주의 |
|---|
| -1 (0xFFFF) | VARIANT_TRUE | true가 아님! |
| 0 | VARIANT_FALSE | false와 같음 |
var.boolVal = true;
var.boolVal = VARIANT_TRUE;
var.boolVal = VARIANT_FALSE;
var.boolVal = condition ? VARIANT_TRUE : VARIANT_FALSE;
6. SAFEARRAY: 자기 기술적 배열
구조
struct SAFEARRAY {
USHORT cDims;
USHORT fFeatures;
ULONG cbElements;
ULONG cLocks;
PVOID pvData;
SAFEARRAYBOUND rgsabound[1];
};
struct SAFEARRAYBOUND {
ULONG cElements;
LONG lLbound;
};
사용 예시
SAFEARRAYBOUND sab = { 10, 0 };
SAFEARRAY* psa = SafeArrayCreate(VT_I4, 1, &sab);
LONG* pData;
SafeArrayAccessData(psa, (void**)&pData);
for (int i = 0; i < 10; i++) {
pData[i] = i * 100;
}
SafeArrayUnaccessData(psa);
VARIANT var;
VariantInit(&var);
var.vt = VT_ARRAY | VT_I4;
var.parray = psa;
VariantClear(&var);
7. C++ 래퍼 클래스
_bstr_t (comutil.h)
#include <comutil.h>
_bstr_t bstr1(L"Hello");
_bstr_t bstr2 = L"World";
_bstr_t bstr3 = bstr1 + L" " + bstr2;
BSTR raw = bstr3;
BSTR detached = bstr3.Detach();
SysFreeString(detached);
| 메서드 | 동작 |
|---|
GetBSTR() | 내부 BSTR 반환 (소유권 유지) |
GetAddress() | BSTR* 반환 (out 파라미터용) |
Detach() | 소유권 이전 |
Attach(BSTR) | 기존 BSTR 소유권 획득 |
copy() | 복사본 생성 (호출자 해제 책임) |
CComBSTR (atlbase.h)
#include <atlbase.h>
CComBSTR bstr1(L"Hello");
CComBSTR bstr2(10);
bstr1.Append(L" World");
bstr1.AppendBSTR(bstr2);
if (bstr1.CompareNoCase(L"HELLO WORLD") == 0) {
}
HRESULT hr = pObj->GetName(&bstr1);
CComVariant (atlbase.h)
CComVariant var1(42);
CComVariant var2(L"Hello");
CComVariant var3(pUnknown);
var1.ChangeType(VT_BSTR);
if (var1 == var2) { }
CComVariant var4;
var4.Copy(&var1);
ComPtr (wrl/client.h) - Modern C++
#include <wrl/client.h>
using Microsoft::WRL::ComPtr;
ComPtr<IUnknown> pUnk;
HRESULT hr = CoCreateInstance(CLSID_Something, nullptr,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pUnk));
ComPtr<IDispatch> pDisp;
hr = pUnk.As(&pDisp);
ComPtr<IStream> pStream;
hr = SHCreateStreamOnFile(L"test.txt", STGM_READ, &pStream);
| 래퍼 | 헤더 | 용도 |
|---|
_bstr_t | comutil.h | BSTR 관리 (가벼움) |
CComBSTR | atlbase.h | BSTR 관리 (ATL) |
CComVariant | atlbase.h | VARIANT 관리 |
CComPtr<T> | atlbase.h | COM 포인터 관리 (ATL) |
ComPtr<T> | wrl/client.h | COM 포인터 관리 (Modern) |
_variant_t | comutil.h | VARIANT 관리 (가벼움) |
8. 흔한 실수와 디버깅
메모리 누수 패턴
| 실수 | 원인 | 해결 |
|---|
| BSTR 누수 | SysFreeString 누락 | 래퍼 클래스 사용 |
| VARIANT 누수 | VariantClear 누락 | CComVariant 사용 |
| COM 객체 누수 | Release 누락 | ComPtr 사용 |
| 이중 해제 | 소유권 혼란 | 규칙 명확화 |
BSTR 디버깅
BSTR bstr = SysAllocString(L"Test");
UINT* pLen = (UINT*)bstr - 1;
힙 손상 원인
| 원인 | 코드 예시 |
|---|
| 잘못된 할당자 | BSTR b = (BSTR)malloc(10); SysFreeString(b); |
| 리터럴 해제 | BSTR b = L"Hello"; SysFreeString(b); |
| 이중 해제 | SysFreeString(b); SysFreeString(b); |
| 버퍼 오버런 | 길이 필드 덮어쓰기 |
Application Verifier
Windows SDK의 Application Verifier로 COM 메모리 문제 감지:
1. Application Verifier 실행
2. 대상 EXE 추가
3. "Basics > Heaps" 활성화
4. "COM" 테스트 활성화
5. 프로그램 실행 → 문제 발생 시 브레이크
9. 프로세스 간 마샬링
왜 마샬링이 필요한가?
COM은 프로세스 경계를 넘어 호출할 수 있다:
| 호출 유형 | 설명 | 마샬링 |
|---|
| In-process | 같은 프로세스 내 DLL | 불필요 |
| Local | 같은 머신, 다른 프로세스 | 필요 |
| Remote | 다른 머신 (DCOM) | 필요 |
BSTR 마샬링
BSTR은 자동으로 마샬링된다:
| 단계 | 동작 |
|---|
| 1. 프록시 | BSTR 길이 읽기 → 버퍼+길이 패킹 |
| 2. RPC | 바이트 스트림 전송 |
| 3. 스텁 | SysAllocStringLen으로 재생성 |
| 4. 호출 | 새 BSTR을 파라미터로 전달 |
length-prefix 덕분에 정확한 크기를 알 수 있어 효율적이다.
커스텀 마샬링
IMarshal 인터페이스로 직접 제어 가능:
interface IMarshal {
GetUnmarshalClass(...);
GetMarshalSizeMax(...);
MarshalInterface(...);
UnmarshalInterface(...);
ReleaseMarshalData(...);
DisconnectObject(...);
};
마치며
COM의 문자열과 메모리 관리는 언어 중립성과 프로세스 경계 투명성을 위해 설계되었다:
| 설계 결정 | 이유 |
|---|
| BSTR (length-prefix) | 바이너리 안전, 마샬링 효율 |
| CoTaskMemAlloc | 모듈 간 메모리 공유 |
| IUnknown::AddRef/Release | 언어 중립 참조 카운팅 |
| VARIANT | 동적 타입, 스크립트 언어 지원 |
| SAFEARRAY | 자기 기술적 배열, 경계 검사 |
현대 C++에서는 래퍼 클래스를 사용하여 이 복잡성을 숨긴다:
| 용도 | 권장 래퍼 |
|---|
| BSTR | _bstr_t 또는 CComBSTR |
| VARIANT | CComVariant 또는 _variant_t |
| COM 포인터 | ComPtr<T> (WRL) 또는 CComPtr<T> |
COM이 1993년에 설계되었음에도 여전히 Windows의 핵심으로 남아있는 이유는, 이러한 견고한 메모리 모델 덕분이다.
더 읽을거리
공식 문서
서적
- Box, D. (1998). Essential COM - COM의 바이블
- Tavares, C. & Ferracchiati, F. (2006). .NET and COM: The Complete Interoperability Guide
- Rector, B. & Sells, C. (1999). ATL Internals
소스 코드