Today I Suffered From (1) - Unity LPWStr(wchar_t*) marshalling causes memory violation
MoonCha, 2019-07-15
Today I Suffered From
최근 Windows Store 출시를 위해 Unity WSA로 빌드를 하게 되었고, IL2CPP를 이용한 결과물이 프로젝트로 나온다. 그리고 이를 기반으로 C++ Plugin을 작성하여 결제를 지원하도록 수정중이다.
Unity에서는 C# 을 사용하는 반면, Plugin에서는 C++을 사용하므로 Plugin의 함수를 호출하거나 return value를 받을 때 C#에서 필연적으로 Marshalling을 하게 되는데, 주로 string
(C#) <–> wchar_t *
(C++) 사이의 Marshalling이 빈번하게 일어났다.
그러던 중, 3336길이를 가지는(= wchar_t
가 2byte이고, null이 포함되므로 총 1667글자) today-i-suffered-from-1을 C++ Plugin에서 반환하고, C# 코드에서 Marshalling된 string을 사용하려 했더니, Memory Access Violation 에러들이 났다. 그런데 매 번 동일한 패턴이 아니고 여러 군데에서 터지는 문제가 발생했다.
이상한 것이 기존에도 wchar_t *
–> string
변환은 이미 작성한 다른 코드들에서 문제 없이 동작하고 있었는데, 이 곳에서 문제가 매 번 발생하는 것으로 보아 길이가 특정 길이 이상이면 메모리 관리가 이상해지는 것으로 추정했다.
일단은 이를 빠르게 수정할 방법은 찾을 수가 없어서, 다른 방법을 찾아보기로 했다. 다른 C++ Plugin에서 아주 긴 문자열도 보낸 기억이 있는데, Unity의 SendMessage
를 이용해 Callback을 부를 때, parameter로 wchar_t *
를 Marshalling하는 경우는 문제가 없는 것이 생각나서 일단 그런 식으로 우회해 두었다.
그래도 일단은 wchar_t *
를 return value로 주는 경우가 문제를 일으키는지 확인해보기 위해 이를 테스트 하기 위한 Minimal Unity Project를 만들었다.
Unity Project 테스트
실험 환경:
- Unity 2018.3.8f1
- x64
먼저, 테스트를 위해 아래와 같이 빈 프로젝트를 만들고 Text UI하나를 놓았다.
그 다음, C++ Plugin에서 받아온 string
을 화면에 보여주도록 만들었다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
public class MarshalledStringTester : MonoBehaviour
{
#if UNITY_WSA && !UNITY_EDITOR
[DllImport("__Internal")]
[return: MarshalAs(UnmanagedType.LPWStr)]
private static extern string getMarshalledString();
#endif
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
#if UNITY_WSA && !UNITY_EDITOR
string marshalledString = getMarshalledString();
GetComponent<UnityEngine.UI.Text>().text = marshalledString;
#endif
}
}
C++ Plugin으로는 아래와 같이 길이를 늘려가면서 wchar_t *
를 반환하게 만들었다.
#include <iostream>
#include <Windows.h>
const wchar_t* toUnityString(const wchar_t* srcData) {
ULONG ulSize = (wcsnlen_s(srcData, 100000) * sizeof(wchar_t)) + sizeof(wchar_t);
wchar_t* pwszReturn = (wchar_t*)::CoTaskMemAlloc(ulSize);
// Copy the contents.
wcscpy_s(pwszReturn, ulSize, srcData);
return pwszReturn;
}
std::wstring astr = L"";
extern "C" {
const wchar_t* _stdcall getMarshalledString() {
astr = astr + L"a";
std::wcout << wcsnlen_s(astr.c_str(), 100000) << std::endl;
return toUnityString(astr.c_str());
}
}
실행 결과
뭔가 실행할 때 마다 다른 곳에서 다양하게 죽는 것을 확인할 수 있다.
정확한 원인은 모르겠지만 Marshaling된 string을 이용할 때 아무튼 메모리가 개판이 되는 것은 확실하다.
이미 Windows Store 관련 개발로 너무 많은 시간을 끌어 버려서 일단은 되는 방법으로 조치해두고, 시간을 더 투자해서 해결법이나 원인을 제대로 찾지는 못해서 Today I Suffered From(TISF)로 제목을 지었다.
2019.07.17
추가 실험
위 실험을 진행하면서 정확히 어떤 크기의 Input이 Memory Violation을 일으키는지 테스트 해보지 않았다. 그래서 이를 알아내기 위해 아래와 같이 고정된 길이의 wchar_t *
값을 변경해가면서 실행한 후 시간이 지나도 Memory Violation을 일으키지 않는지 확인해 보았다.
#include <iostream>
#include <Windows.h>
const wchar_t* toUnityString(const wchar_t* srcData) {
ULONG ulSize = (wcsnlen_s(srcData, 100000) * sizeof(wchar_t)) + sizeof(wchar_t);
wchar_t* pwszReturn = (wchar_t*)::CoTaskMemAlloc(ulSize);
// Copy the contents.
wcscpy_s(pwszReturn, ulSize, srcData);
return pwszReturn;
}
extern "C" {
const wchar_t* _stdcall getMarshalledString() {
return toUnityString(L"a");
}
}
테스트 결과는 다음과 같다.
- a - OK
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - NO
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - NO
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - NO
- aaaaaaaaaaaaaaaaaaaaaaaaaa - OK
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - OK
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - NO
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - NO
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - NO
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - OK
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - NO
위 결과 중 마지막 두 개에서 어떤 길이 이상의 문자열이 문제를 일으키는지 확인할 수 있다. 37글자의 a의 경우 OK 38글자의 a의 경우 Memory Violation이 발생했다.
따라서 이로 부터 추측해볼 때, 37글자를 초과하는 경우 Memory Violation이 발생하는 것으로 보인다.
string의 in-memory size에 관한 글에 따르면 37글자의 string
은 x64 환경에서 26 + length * 2 = 100 byte 인 것으로 보인다.
처음에는 문자열의 길이에 따라 C#에서 string
이 어느 heap bin에 할당되는지에 따라 문제가 있지 않을까? 라고 의심을 했는데
100은 대단히 수상한 숫자여서 Unity의 Marshalling 로직에 오류가 있지는 않은지 킹리적 갓심을 해본다.