C++/CLI 코드를 작성하다가 괴상한, 아니 능력이 모자라서 이해 못하는 현상을 발견해서 이 블로그 구독자들의 도움을 받아볼까 합니다. 우선 다음 코드를 봐주세요.
inline MyString marshal_as(System::String^ const & _from_obj) { if(_from_obj != nullptr) { cli::pin_ptr<const wchar_t> _pinned_ptr = PtrToStringChars(_from_obj); MyString _to_obj(static_cast<const wchar_t *>(_pinned_ptr), _from_obj->Length); return _to_obj; } else { throw gcnew System::ArgumentNullException(_EXCEPTION_NULLPTR); } } static CClassManaged^ FindClass(System::String^ className) { // StaticFindClass의 CHECK에서 걸린다. const TCHAR* const classNameNative = marshal_as(className).c_str(); // 작동한다 // MyString ss = marshal_as(className); // const TCHAR* const classNameNative = ss.c_str(); CClass* classObj = CClass::StaticFindClass(classNameNative); return gcnew CClassManaged(classObj); } CClass* CClass::StaticFindClass(const TCHAR* const ClassName) { std::map<MyString, CClass*>::const_iterator It = GClassMap.find(ClassName); CHECK(It != GClassMap.end()); if (It == GClassMap.end()) return NULL; return It->second; } CClassManaged^ managedClass = CClassManaged::FindClass("DBObject_SessionInfo");
우선 사전지식부터 설명해보겠습니다. MyString 클래스는 사실상 std::wstring과 같습니다. 메모리 할당자만 바꾼 문자열 클래스입니다. marshal_as 는 <msclr/marshal_cppstd.h>에 구현된 코드와 동일한데, std::wstring이 MyString으로 바뀐 것만 다릅니다.
맨 아래 CClassManaged::FindClass 메서드를 호출하는 코드가 시작점입니다. 닷넷 문자열(System::String)을 받아서 C 문자열(TCHAR*)로 변환합니다. 이때 문자열 변환을 맡는 메서드가 marshal_as입니다. marshal_as는 MyString을 반환하는데, 이렇게 반환한 문자열을 다른 MyString 인스턴스에 할당한 다음, MyString::c_str()을 호출하면 괜찮습니다.
한데 명시적으로 할당하지 않고 곧장 MyString::c_str()을 호출하면 큰일이 납니다. std::map에서 문자열을 찾는 도중에(GClassMap.find), CHECK 조건에 걸려버립니다. 왜 그런가 했더니 GClassMap.find을 나오는 순간 ClassName 값이 Empty("")가 되어버렸기 때문입니다.
어째서 이런 일이 벌어지는 것일까요?
Author Details
Kubernetes, DevSecOps, AWS, 클라우드 보안, 클라우드 비용관리, SaaS 의 활용과 내재화 등 소프트웨어 개발 전반에 도움이 필요하다면 도움을 요청하세요. 지인이라면 가볍게 도와드리겠습니다. 전문적인 도움이 필요하다면 저의 현업에 방해가 되지 않는 선에서 협의가능합니다.
신기한 버그인데
직접 코드를 실행보고 어떻게 어셈블러 코드를 보면 확인가능하겠지만
제가 생각하기에는
return 된값은 스택에 남아있거든요????
//아래것은 스택에서 호출하기때문에
//className이라는 인수전달이 꼬여버리는거
//아닐까 합니다.
//그니까 MyString클래스에 className을 전달하는과정이 빠져버렸다 이거죠
const TCHAR* const classNameNative = marshal_as(className).c_str();
//아랫것은 스택에서 ss로 카피된후
//하기 때문에 잘되는것이 아닐까합니다.
//즉 오퍼레이터 =에 의해 className이라는 인수가
//전달됩니다.
// 작동한다
// MyString ss = marshal_as(className);
// const TCHAR* const classNameNative = ss.c_str();
const TCHAR* const classNameNative = marshal_as(className).c_str();
에서 marshal_as는 MyString의 임시객체를 반환합니다. 이 임시객체는 생성된 라인을 벗어나면 소멸하게 됩니다. 그러면 classNameNative의 포인터는 객체의 소멸과 동시에 무효화되어 의미없는 포인터가 됩니다. 무효화된 포인터를 사용하는 것은 예기치 못한 동작을 일으킵니다.
주석처리하신 작동하는 코드는 ss객체의 스코프가 FindClass가 리턴할 때 까지 유지되기 때문에 정상적인 동작을 했던 것 입니다.
아, 그렇군요. 임시객체 소멸 시점을 잘못 알고 있었습니다. 예전에도 이런 걸 당해본 듯한 느낌도 들긴 하는데, 원래 저런 코드를 거의 안 짜서 매번 당하는 듯 하네요. 디버깅할 때 쉽게 하려고 보통은 marshal_as(className).c_str(); 표현을 거의 안 쓰거든요.
매우 실수하기 쉬운 버그 중에 하나이네요. 정덕환님 말씀대로 marshal_as는 클래스 객체 자체를 리턴하는데 이것이 그 어디에도 저장이 되지 않는 임시 객체이기 때문에, 20번째 라인을 실행하고 나면 이 객체는 소멸됩니다. 이건 C++/CLI과 상관없는 일반적인 C++ 문법 규약이므로 native C++에서 비슷한 코드를 작성하시고 disassemble을 보시면서 디버깅을 진행해보시면 명확히 아실 것입니다. 20번째 줄 뒤에 MyString의 소멸자가 불려질 것입니다.
그리고 같은 이유로 //작동한다 아래의 코드도 FindClass를 벗어나면 망가집니다. 그래서 TCHAR*를 이 함수 밖으로 전달하는 것도 버그를 유발합니다. 이런 버그 주의하세요~
닷넷 객체하고 C++ 객체하고 섞어 쓰다보면 정신이 혼미해집니다. -_-;;