c++ 캐릭터 배열을 c# 배열로 마셜링하기

아래 c++ 구조체 안의 캐릭터 배열을 c# 배열로 마셜링하는 데에는 UnmanagedType.ByValArray와 UnmanagedType.ByValTStr을 쓸 수 있다.

typedef struct TestStruct
{
    char TestChars[5];
}

UnmanagedType.ByValArray

ByValArray를 쓰면 똑같이 캐릭터 배열로 바꾼다.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public class TestClass
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public char[] TestChars;
}

c의 캐릭터는 ansi로서 1바이트를 차지하고 c#의 그것은 유니코드이며 크기가 2바이트다. 전자에서 한글을 표기하려면 두 개의 캐릭터들이 필요하다. 위의 c++ 예에서 구조체에는 한글 두 글자들만 들어갈 수 있으며 예를 들면 ‘가나\32(공백)’이다. 이걸 위의 c# 코드로 받으면 ‘가나\32\0\0(널)’로 바뀐다. c#의 유니코드 캐릭터는 캐릭터 하나가 한글 한 자를 표기할 수 있기 때문에 ‘가나\32’로 끝나면 세 개의 캐릭터들밖에 되지 않으므로 나머지 두 자리들은 널들로 채워진다. 출력하면 두 경우 모두 ‘가나 ‘로 같게 보인다.

UnmanagedType.ByValTStr

문자열을 그냥 보기만 할 거면 위의 경우 문제될 게 없는데 다양한 목적으로 처리를 해야 한다면 널 때문에 문제가 생길 수 있다. 예를 들어 뒤의 공백을 없애기 위해 string.trim()을 실행하면 널에 막혀서 공백이 잘려나가지 않는다. 이 문제는 ByValTStr를 써서 해결할 수 있다.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public class TestClass
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 5)] public string TestString;
}

위의 코드를 실행하면 변수에 ‘가나\32’가 만들어진다. 그러나 여기에도 문제가 있다. c# string에 ‘abcde’를 저장하고 위의 코드를 이용하여 c의 구조체로 보내면 ‘abcd\0’이 전달된다. 맨 마지막 자리를 널로 바꾼다. 어차피 받는 쪽에서는 string이 아니라 char 배열이므로 널로 끝나는 문자열이 아니라 해도 알아서 제 자리에 들어갈 테지만 컴파일러는 받는 쪽이 뭔지를 알 수 없다. 그러니 안전빵으로 널 터미네이트를 한다. 이 문제를 피하려면 SizeConst에 1을 더하면 된다. 위 예제에서 6으로 하면 ‘abcde\0’이 전달되고 여섯 번째 캐릭터인 널은 무시된 채 앞의 다섯 개들만 캐릭터 배열에 담겨진다. 번거롭다.

c++로 보낼 때에는 ByValArray를 쓰는 게 편하고 c#으로 받을 때에는 ByValTStr이 좋다.

MarshalAsAttribute.SizeConst

c++의 캐릭터 배열을 c# 배열로 받기 위해 마셜링을 할 때에는 그 크기를 특정해야 한다. 그런데 이렇게 만들어진 캐릭터 배열의 크기를 나중에 확인해야 할 때가 생긴다. 예를 들면 그 크기에 맞는 값을 채워 넣어야 할 경우다. 애플리케이션이 작동하는 동안 이럴 일이 생기면 코드를 보고 확인할 수 없으니 아래와 같이 런타임 확인을 해야 한다.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public class Class1
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] 
    public char[] Chars;
}

private void Form1_Load(object sender, EventArgs e)
{
    Class1 class1 = new();

    FieldInfo fieldInfo = typeof(Class1).GetField("Chars");

    MarshalAsAttribute marshalAsAttribute = fieldInfo.GetCustomAttribute<MarshalAsAttribute>();

    Text = marshalAsAttribute.SizeConst.ToString(); // 5
}

위의 예제와 달리 필드들이 여럿인 때에는 위에 이용된 메떠드들의 이름 뒤에 s가 붙는 것들을 이용하면 된다. 이것들은 배열을 반환한다.