포인터 형 매개 변수를 갖는 C++ DLL의 함수를 C#에서 호출하는 방법
종종 다음과 같은 질문을 보게 되는데요.
C++ dll C#에서 사용하는데 보호된 메모리 오류 떠요 한 번만 도와주세요ㅜ
; https://www.sysnet.pe.kr/3/0/4997
이쯤에서 한번 정리를 해봐야겠습니다. ^^
우선, C/C++ DLL의 함수를 정상적으로 호출하려면 Calling Convention이 맞아야 합니다. 이에 대해서는 지난 글에 잘 정리해 두었으니 참고하시고.
C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (1) - x86 환경에서의 __cdecl, __stdcall에 대한 Name mangling
; https://www.sysnet.pe.kr/2/0/11132
일단 호출 규약이 맞았다면 이제 매개 변수에 대한 인자 값을 잘 맞춰주는 문제만 남습니다. 이때 C# 개발자들이 어려워하는 것 중의 하나가 바로 C/C++의 포인터를 받는 함수에 대한 interop입니다. 이를 이해하려면 C/C++에서 포인터 변수를 받는 방식에 대한 이해가 필요합니다.
가령 C#에서 다음과 같은 코드를 보겠습니다.
static unsafe void Main(string[] args)
{
int value = 5;
PassByValue(value);
Console.WriteLine(value); // 출력 결과 5
PassByRef(ref value);
Console.WriteLine(value); // 출력 결과 50
}
private static void PassByValue(int value)
{
value = 50;
}
private static void PassByRef(ref int value)
{
value = 50;
}
저런 코드를 C/C++에서는 다음과 같이 작성할 수 있습니다.
int value = 5;
PassByValue(value);
printf("%d\n", value); // 출력 결과 5
PassByPtr(&value);
printf("%d\n", value); // 출력 결과 50
void PassByValue(int value)
{
value = 50;
}
void PassByPtr(int *value)
{
*value = 50;
}
즉, C#의 ref와 같은 역할을 위해 포인터 변수(int *)를 넘겨줌으로써 다룰 수 있게 하는 것입니다. 그런데, 문제는 C/C++의 경우 포인터와 배열의 차이점이 없다는 것입니다. 그래서 다음 코드에서 보는 것처럼 호출 측에서 동일하게 &value 포인터 변수를 전달했지만,
int value = 5;
PassByPtr(&value);
printf("%d\n", value); // 출력 결과 50
PassByArray(&value);
printf("%d\n", value); // 출력 결과 500
void PassByPtr(int *value) // 포인터로도 받을 수 있고,
{
*value = 50;
}
void PassByArray(int value[]) // 배열로도 받을 수 있음.
{
value[0] = 500;
}
받는 측의 함수에서는 &value 변수를 int *와, int []로 취향에 맞게 처리할 수 있습니다. 이 때문에, 표면상으로는 포인터를 받는 함수일지라도 내부적으로 그것을 배열로 처리할 수도 있습니다.
int value = 5;
PassByPtr(&value);
printf("%d\n", value); // 출력 결과 500
void PassByPtr(int *value)
{
value[0] = 500;
}
대개의 경우, 포인터(또는 배열)를 받는 함수가 그것을 내부적으로 배열로 다룬다면 안전하게 접근할 수 있도록 명시적으로 배열의 수를 함께 전달할 수 있게 만듭니다.
int value = 5; // 단일 변수이지만,
PassByPtr(&value, 1); // 1개의 배열로써 전달
int array[10] = { 0 }; // 10개의 요소를 갖는 배열을,
int *pArray = &array[0];
PassByPtr(pArray, 10); // 명시적으로 10개라고 지정
void PassByPtr(int *value, int len)
{
for (int i = 0; i < len; i ++)
{
value[i] = 500;
}
}
물론 이것은 해당 함수를 개발하는 프로그래머의 결정에 따르기 때문에 꼭 저런 식으로 개발되었다고 장담할 수 없습니다. 실제로 과거에 많은 수의 C/C++ 함수들 중 문자열 처리의 경우 입력 버퍼의 수를 명시하지 않고 널(\0) 문자를 인식하는 것으로 작성했기 때문에,
char buf[256];
strcpy(buf, "test"); // strcpy 함수는 buf의 배열 크기를 알지 못함
수많은 버퍼 오버런 취약점이 발생하는 원인이 되었습니다. 그래서 나중에는 좀 더 안전한(secure) 함수라고 해서 배열의 크기를 명시하는 버전이 나오게 된 것입니다.
char buf[256];
strcpy_s(buf, 256, "test");
자, 그럼 C/C++ 개발자가 DLL을 만들어 제공하는 함수의 시그니처를 다음과 같이 전달해 줬다고 가정해 보겠습니다.
void ExternC_STD_Func_Ptr(int *value);
C# 개발자는 위의 시그니처를 보면 반드시 C/C++ 개발자에게 물어봐야 합니다. 저 value가 배열이냐? 아니면 순수하게 단일 int 형에 대한 포인터냐? 라고! (물어볼 개발자가 없다면 문서라도 꼼꼼하게 확인해야 합니다.)
만약 단일 int 형에 대한 포인터라면 C#에서 다음과 같이 처리할 수 있습니다.
[DllImport("Win32Project1.dll", SetLastError = true)]
internal static unsafe extern int ExternC_STD_Func_Ptr(int *value);
static unsafe void Main(string[] args)
{
int value = 5;
int* pValue = &value;
ExternC_STD_Func_Ptr(pValue);
}
하지만 배열이라고 한다면, 그 배열의 크기를 묻고 그에 맞게 전달해야 합니다.
[DllImport("Win32Project1.dll", SetLastError = true)]
internal static unsafe extern int ExternC_STD_Func_Ptr(int *value);
static unsafe void Main(string[] args)
{
int[] array = new int[10];
fixed (int* pArray = &array[0])
{
ExternC_STD_Func_Ptr(pArray);
for (int i = 0; i < array.Length; i ++)
{
Console.Write(array[i] + ",");
}
Console.WriteLine();
}
}
포인터 변수에 대해 위의 규칙 정도만 맞춰주면 AV(Access Violation) 오류로 인한 비정상 종료 문제는 사라질 것입니다.
(
첨부 파일은 이 글의 예제 - C#, C/C++ 코드를 포함합니다.)
참고로 COM DLL에 대해서는 다음의 글도 도움이 될 것입니다.
[in, out] 배열을 C#에서 C/C++로 넘기는 방법 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/811
[in, out] 배열을 C#에서 C/C++로 넘기는 방법
; https://www.sysnet.pe.kr/2/0/810
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]