Search
Duplicate

C++/ C++ DLL을 C#에서 사용하기

C#에서 C++ DLL 선언

C#에서 C++ DLL를 사용하려면 extern 키워드를 이용해서 C++ DLL의 함수를 선언해 줘야 한다. 선언 하는 방법은 아래 코드와 같다. 만일 C#에서 pointer(*)를 넘겨야 하는 경우가 있다면 unsafe 키워드를 추가로 붙여준다.
// DllImport에 지정되는 이름은 C++ DLL의 이름과 경로를 의미한다. [DllImport("Test.dll")] static extern int Sum(int num1, int num2); // *를 사용하고 있으므로 unsafe 키워드를 붙여 준다. [DllImport("Test.dll")] unsafe static extern bool Detect(byte* source);
C#
복사

기본 타입 전달하고 return 받기

int나 double 같은 기본 타입에 대해서는 알아서 변환이 되므로 일반적으로 코드를 작성하는 것처럼 사용하면 된다.
// C++ 코드 int AddInt(const int val1, const int val2) { return val1 + val2; } double MultiplyDouble(const double val1, const double val2) { return val1 * val2; }
C++
복사
// C# 코드 int numInt = AddInt(val1: 1, val2: 2); // 결과는 3 double numDouble = MultiplyDouble(val1: 2d, val2: 0.5d); // 결과는 1
C#
복사

구조체 전달하고 return 받기

구조체는 C#과 C++에서 형식과 그 순서를 맞춰야 한다는 것만 제외하면 사용하는 법은 기본 타입과 동일하다. 우선 C++과 C#의 구조체는 다음과 같이 맞추면 된다.
// C++ 코드 struct StructSample { public: StructSample(int numInt, double numDouble) : numInt(numInt), numDouble(numDouble) { } int GetNumInt() const { return this->numInt; } int GetNumDouble() const { return this->numDouble; } private: int numInt; double numDouble; };
C++
복사
// C# 코드 [StructLayout(LayoutKind.Sequential)] struct StructSample { public StructSample(int numInt, double numDouble) { this.NumInt = numInt; this.NumDouble = numDouble; } public int NumInt { get; private set; } public double NumDouble { get; private set; } };
C#
복사
C#에서 구조체에 특성으로 LayoutKind를 Sequential로 놓고 C++과 C#의 구조체 내부 순서를 맞추면 문제 없이 사용 가능하다. —class로는 안 된다.
위의 예시는 일부러 생성자와 C++에서 Get 함수, C#에서 Property를 구성하였는데, 이렇게 해도 동작 가능하다는 것을 보여주기 위해 한 것이다.
// C++ 코드 StructSample MultiplyStructure(const StructSample& input) { return StructSample(input.GetNumInt() * 2, input.GetNumDouble() * 2.0); }
C++
복사
// C# 코드 StructSample input = new StructSample(numInt: 1, numDouble: 2d); StructSample output = NativeMethod.MultiplyStructure(input: input); // 결과는 2, 4
C#
복사

C#에서 C++로 string 전달하기

C#과 C++의 string은 이름은 같지만 그 구현은 전혀 다른 것이다. 그러나 둘 다 char로 된 배열이라는 점이 같으므로 C++의 인자 쪽을 char* 타입으로 하여 C#의 string을 C++로 전달할 수 있다.
그렇게 전달 받은 char*를 C++ 내에서는 string 타입으로 변환하여 사용하면 된다.
// C++ 코드 int AddStringLength(const char* val1, const char* val2) { string str1(val1); string str2(val2); return str1.size() + str2.size(); }
C++
복사
// C# 코드 string str1 = "hello"; string str2 = "world"; // C++ 쪽에서 char*로 받으면 C#에서는 string으로 넘겨도 알아서 변환 된다. int output = NativeMethod.AddStringLength(val1: str1, val2: str1); // 결과는 10
C#
복사
C#에서 C++로 string을 전달하는 것은 간단하지만, C++에서 C#으로 string을 반환하는 것은 상당히 까다롭다. string은 엄밀히 말해 char의 배열이기 때문에, 이에 대해서는 이하 C++에서 C#으로 배열 전달 받기 부분에서 다룬다.
// C++ 코드 struct StructSampleStr { public: // string을 직접 받을 수 없기 때문에 char* 로 받는다. StructSampleStr(int numInt, double numDouble, char* str) : numInt(numInt), numDouble(numDouble), str(str) { } int GetNumInt() const { return this->numInt; } int GetNumDouble() const { return this->numDouble; } // C++ 내부에서 이 구조체를 사용할 때는 char*를 string으로 변환해서 사용한다. string GetStr() const { return this->str; } private: int numInt; double numDouble; char* str; };
C++
복사
// C# 코드 [StructLayout(LayoutKind.Sequential)] struct StructSampleStr { public StructSampleStr(int numInt, double numDouble, string str) { this.NumInt = numInt; this.NumDouble = numDouble; this.Str = str; } public int NumInt { get; private set; } public double NumDouble { get; private set; } public string Str { get; private set; } };
C#
복사
사용은 다음과 같다.
// C++ 코드 int GetStructureNum(const StructSampleStr& input) { return input.GetNumInt() * input.GetNumDouble() * input.GetStr().size(); }
C++
복사
// C# 코드 StructSampleStr input = new StructSampleStr(numInt: 1, numDouble: 2d, str: "hello"); int output = NativeMethod.GetStructureNum(input: input); // 결과는 10
C#
복사

C#에서 C++로 1차원 배열 전달하기

C#에서 C++로 배열을 전달하는 것도 알아서 변환이 되므로 일반적인 코드를 작성하는 것처럼 작성할 수 있다. 다만 C++에서는 언어 자체의 특성 상 C# 처럼 배열을 인자로 받지 못하기 때문에 pointer로 선언해야 하며, pointer 로 인자를 받는 경우 그 길이를 알 수 없기 때문에 추가로 길이 정보도 함께 보내줘야 한다.
// C++ 코드 void UpdateNumArray(int* numArr, int length) { for (int i = 0; i < length; i++) { numArr[i] *= 2; } }
C++
복사
// C# 코드 // List와 같은 C#의 generic은 C#에서만 쓰이는 것이므로 배열로 전달해야 한다. int[] numArr = { 1, 2, 3, }; NativeMethod.UpdateNumArray(numArr: numArr, length: numArr.Length); // 결과는 2, 4, 6
C#
복사
구조체 배열도 마찬가지로 할 수 있다.
// C++ 코드 // 포인터로 구조체의 배열을 받는다. int SumStruectureNum(const StructSample* inputs, const int length) { int sum = 0; for (int i = 0; i < length; i++) { sum += inputs[i].GetNumInt() * inputs[i].GetNumDouble(); } return sum; }
C++
복사
// C# 코드 StructSample[] inputs = new StructSample[3]; inputs[0] = new StructSample(numInt: 1, numDouble: 2d); inputs[1] = new StructSample(numInt: 2, numDouble: 2d); inputs[2] = new StructSample(numInt: 3, numDouble: 2d); // 배열 형태로 넘기면 알아서 변환 된다. int output = NativeMethod.SumStruectureNum(inputs: inputs, length: inputs.Length); // 결과는 12
C#
복사

C++에서 C#으로 1차원 배열 return 하기

상대적으로 간단한 C#에서 C++로 배열 전달하는 것과 달리 C++에서 C#으로 배열을 전달하는 것은 상당히 까다롭다. 이것은 애초에 C++이 언어 차원의 제약 때문이다.
이것을 이해하려면 C#과 C++의 메모리 관리의 차이를 이해할 필요가 있는데, 값형 (int, double, struct 등) 데이터를 stack에, 참조형 (string, array, class 등) 데이터를 heap에서 관리하는 C#과 달리 C++은 C#에서 참조형이라고 불리는 데이터도 모두 stack에 올릴 수 있다. —C# 으로 프로그래밍을 시작해서 C++을 역으로 접한 나에게 가장 놀라웠던 점이 바로 이 점이었다. 이 때문에 C++에서는 복사 비용도 고려해서 코드를 작성해야 한다.
stack에 존재하는 배열을 return 하는 경우, 함수 블록이 종료된 후에 배열이 파괴되기 때문에 함수 바깥에서는 값이 비어 있는 —혹은 쓰레기 값으로 차 있는— 배열을 전달 받게 된다. 다음 코드를 보자.
int* GetArray() { // 이 배열은 stack에 올라간다. int arr[10]; for (int i = 0; i < 10; i++) { arr[i] = i; } // 함수 블록이 종료되면 stack의 데이터는 모두 파괴된다. return arr; } int main() { int* arr = GetArray(); for (int i = 0; i < 10; i++) { // 0 - 9까지의 숫자가 아니라 쓰레기 값이 나온다. cout << arr[i] << endl; } }
C++
복사
이를 피하기 위해 C++에서는 배열을 heap에 올리고 그 heap을 가리키는 pointer를 리턴하거나 —이 경우 heap 메모리 생성은 함수 안에서 했지만, 메모리 해제는 함수 밖에서 하는 위험한 상황이 연출 되므로 지양하는 것이 좋다— 함수 밖에서 배열을 선언하고, 함수에는 배열의 참조(&)를 전달해서 배열 내부의 값을 채우는 식으로 한다. —이게 귀찮기 때문에 보통은 그냥 STL을 쓴다.
// 배열을 포인터로 전달하는 경우, 배열의 길이를 알 수 없기 때문에 추가로 길이 인자를 보낸다. void UpdateArray(int* arr, int size) { for (int i = 0; i < size; i++) { arr[i] = i; } } int main() { // c++ 특성상 배열에는 상수 리터럴을 써야 한다. 만일 변수를 이용해서 배열의 길이를 동적으로 하고 싶다면 new를 해서 배열을 heap에 올려야 한다. int arr[10] UpdateArray(arr, 10); for (int i = 0; i < 10; i++) { // 0 - 9까지의 숫자가 제대로 나온다. cout << arr[i] << endl; } }
C++
복사
이것은 C++의 특성이기 때문에 C#에서 배열로 결과를 받기 위해서는 배열을 감싸는 구조체를 만들어서 사용하는 방법을 사용해야 한다. —배열을 참조로 넘기는 경우는 C++ 내부에서 new 를 해야 하는데, 이 경우 해당 메모리를 C#에서는 해제할 방법이 없기 때문에 사용할 수 없다.

크기가 고정인 1차원 배열을 return

결국 C# CLR에서 C++의 DLL을 부르는 형식이기 때문에 CLR이 변환 형식과 그 크기를 알고 있으면 쉽다. 위의 예제에서 string을 char*로 변환해 주는 것이나, 배열을 포인터(*)로 알아서 변환해 주는 것도 결국 CLR 이 무엇을 얼마만큼 변환해야 하는지 알기 때문에 가능한 것이다.
비슷한 맥락으로 만일 CLR이 C++에서 받아야 하는 배열의 크기를 미리 알고 있다면, C++에서 C#으로 쉽게 전달 가능하다. 배열의 크기를 미리 알고 있다는 것은 동적으로 생성된 배열이 아니며, 상호 규약에 의해 사전에 정의된 크기를 갖는다는 이야기다.
앞선 구조체를 다음과 같이 변경한다.
// C++ 코드 struct StructSampleFixed { public: // string을 직접 받을 수 없기 때문에 char* 로 받는다. StructSampleFixed(int numInt, double numDouble, char* source) : numInt(numInt), numDouble(numDouble) { strcpy_s(str, source); } int GetNumInt() const { return this->numInt; } int GetNumDouble() const { return this->numDouble; } // C++ 내부에서 이 구조체를 사용할 때는 char*를 string으로 변환해서 사용한다. string GetStr() const { return this->str; } private: int numInt; double numDouble; // char 배열의 크기(255)를 고정한다. 이 크기는 C#에서와 동일해야 한다. char str[255]; };
C++
복사
// C# 코드 [StructLayout(LayoutKind.Sequential)] struct StructSampleFixed { public StructSampleFixed(int numInt, double numDouble, string str) { this.NumInt = numInt; this.NumDouble = numDouble; this.Str = str; } public int NumInt { get; private set; } public double NumDouble { get; private set; } // string을 ByValTStr 이라는 타입으로 마샬링하고, Size는 C++과 같은 크기(255)를 지정한다. [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)] public string Str; // 아쉽게도 마샬링은 Property에 대해서는 동작하지 않는다. 때문에 public 변수로 선언해야 함. };
C#
복사
사용은 다음과 같다.
// C++ 코드 StructSampleFixed UpdateStructureFixed(const StructSampleFixed& input) { int numInt = input.GetNumInt() * 2; double numDouble = input.GetNumDouble() * 2.0; string text = input.GetStr() + " world"; // 고정된 크기를 사용한다. char str[255]; strcpy_s(str, text.c_str()); return StructSampleFixed(numInt, numDouble, str); }
C++
복사
// C# 코드 StructSampleFixed input = new StructSampleFixed(numInt: 1, numDouble: 2d, str: "hello"); StructSampleFixed output = NativeMethod.UpdateStructureFixed(input: input); // 결과는 2, 4, "hello world"
C#
복사

크기가 가변인 1차원 배열을 return

크기를 고정할 수 있는 경우가 아니라면 동적 할당을 사용 할 수 밖에 없다. 이것은 C++에서 배열을 new 하여 heap에 올리는 것인데, 이 경우 해당 메모리는 C#에서 제어할 수 없는 영역에 존재하기 때문에, 이를 제거하는 코드도 C++에 만들어서 C#에서 호출할 수 있게 해야 한다. 그렇지 않으면 그 유명한 메모리 누수(memory leak)가 발생한다.
추가로 크기가 가변인 배열은 C#의 CLR도 크기를 알 수 없어서 배열을 자동으로 변환할 수 없기 때문에, 배열을 IntPtr 형태로 받은 후에 Marshaling 하는 방식으로 처리해야 한다.
우선 C++과 C#에 다음과 같이 구조체를 만든다.
// C++ 코드 struct StructSampleDynamic { public: StructSampleDynamic(int sizeInt, int sizeDouble, int* arrInt, double* arrDouble, char* str) : sizeInt(sizeInt), sizeDouble(sizeDouble), arrInt(arrInt), arrDouble(arrDouble), str(str) { } int GetSizeInt() const { return this->sizeInt; } int GetSizeDouble() const { return this->sizeDouble; } int* GetArrInt() const { return this->arrInt; } double* GetArrDouble() const { return this->arrDouble; } char* GetArrChar() const { return this->str; } // 메모리 해제를 위해 포인터를 반환 private: int sizeInt, sizeDouble; int* arrInt; double* arrDouble; char* str; };
C++
복사
// C# 코드 [StructLayout(LayoutKind.Sequential)] struct StructSampleDynamic { // string은 Marshaling으로 처리할 수 있으므로 size 정보가 없어도 된다. public StructSampleDynamic(int sizeInt, int sizeDouble, IntPtr ptrInt, IntPtr ptrDouble, IntPtr ptrStr) { this.SizeInt = sizeInt; this.SizeDouble = sizeDouble; this.PtrInt = ptrInt; this.PtrDouble = ptrDouble; this.PtrStr = ptrStr; } public int SizeInt { get; private set; } public double SizeDouble { get; private set; } // C++에서 동적으로 생성한 배열을 받기 위해 IntPtr 타입을 사용한다. public IntPtr PtrInt { get; private set; } public IntPtr PtrDouble { get; private set; } public IntPtr PtrStr { get; private set; } };
C#
복사
사용은 다음과 같다.
// C++ 코드 StructSampleDynamic GetStructureDynamic() { int sizeInt = 10; int* arrInt = new int[sizeInt]; for (int i = 0; i < sizeInt; i++) { arrInt[i] = i; } int sizeDouble = 5; double* arrDouble = new double[sizeDouble]; for (int i = 0; i < sizeDouble; i++) { arrDouble[i] = i * 0.1; } string text("hello world"); char* str = new char[text.size() + 1]; // new를 해야 C#으로 데이터를 넘길 수 있다. std::copy(text.begin(), text.end(), str); // string을 char 배열로 복사 str[text.size()] = '\0'; // char 배열의 끝에 널 문자 추가 return StructSampleDynamic(sizeInt, sizeDouble, arrInt, arrDouble, str); } // C++ 코드 내부에서 new 를 하고 있으므로 delete 하는 코드도 만들어줘야 한다. bool DeleteStructDynamicArr(const StructSampleDynamic& sample) { delete[] sample.GetArrInt(); delete[] sample.GetArrDouble(); delete[] sample.GetArrChar(); return true; }
C++
복사
// C# 코드 // C++ 코드에서 구조체를 받아온다. // 이 안에는 C++에서 new 를 한 데이터가 있으며, 이 데이터는 C# 에서는 해제할 수 없다. StructSampleDynamic sample = NativeMethod.GetStructureDynamic(); // int나 double 같은 기본 타입은 아래와 같이 Marshal.Copy를 통해 처리할 수 있다. int[] arrInt = new int[sample.SizeInt]; Marshal.Copy(sample.PtrInt, arrInt, 0, sample.SizeInt); // arrInt 결과는 { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } double[] arrDouble = new double[sample.SizeDouble]; Marshal.Copy(sample.PtrDouble, arrDouble, 0, sample.SizeDouble); // arrDouble 결과는 { 0, 0.1, 0.2, 0.3, 0.4 } // string은 PtrToString... 시리즈를 이용하면 쉽게 변환 가능하다. string str = Marshal.PtrToStringAnsi(sample.PtrStr); // str 결과는 "hello world" // C++에서 new 한 데이터를 delete 하기 위해 다시 C++에 만들어 놓은 Delete 함수를 호출한다. NativeMethod.DeleteStructDynamicArr(sample);
C#
복사

C#에서 C++로 내부에 배열을 가진 구조체를 전달하기

앞선 코드들에서 구조체를 전달하는 것과 배열을 전달하는 것을 모두 처리해 보았다. 그렇다면 구조체 내부에 있는 배열들은 어떻게 전달할 수 있을까? string이 처리되는 것을 보면 구조체 내부의 배열도 마찬가지로 처리할 수 있을 것 같다.
예컨대 C++의 구조체는 앞서 정의한 StructSampleDynamic과 같다고 하고, C#에서 아래와 같이 구조체를 선언했다고 가정하자.
// C# 코드 [StructLayout(LayoutKind.Sequential)] struct StructSampleArr { public StructSampleArr(int sizeInt, int sizeDouble, int[] arrInt, double[] arrDouble, string str) { this.SizeInt = sizeInt; this.SizeDouble = sizeDouble; this.ArrInt = arrInt; this.ArrDouble = arrDouble; this.Str = str; } public int SizeInt { get; private set; } public double SizeDouble { get; private set; } // C++에서 동적으로 생성한 배열을 받기 위해 IntPtr 타입을 사용한다. public int[] arrInt { get; private set; } public double[] arrDouble { get; private set; } public string Str { get; private set; } };
C#
복사
이 구조체는 C++에서 정의한 StructSampleDynamic과 형식과 순서가 동일하게 대응된다. 따라서 이름은 다르지만 C++로 넘기는 것은 전혀 문제가 없다.
앞서 크기가 가변인 1차원 배열을 C++에서 return 받는 코드를 다음과 C#에서 넘겨 받은 데이터를 이용해서 처리하도록 수정해 보자.
// C++ 코드 StructSampleDynamic GetStructureDynamic(const StructSampleDynamic& input) { // size를 input에서 가져온다. int sizeInt = input.GetSizeInt(); int* arrIntInput = input.GetArrInt(); int* arrInt = new int[sizeInt]; for (int i = 0; i < sizeInt; i++) { // input 데이터를 이용해서 output을 만든다. arrInt[i] = arrIntInput[i] * 2; } int sizeDouble = input.GetSizeDouble(); double* arrDoubleInput = input.GetArrDouble(); double* arrDouble = new double[sizeDouble]; for (int i = 0; i < sizeDouble; i++) { arrDouble[i] = arrDoubleInput[i] * 0.5; } // string을 처리하는 코드는 이전과 동일 string text("hello world"); char* str = new char[text.size() + 1]; // new를 해야 C#으로 데이터를 넘길 수 있다. std::copy(text.begin(), text.end(), str); // string을 char 배열로 복사 str[text.size()] = '\0'; // char 배열의 끝에 널 문자 추가 return StructSampleDynamic(sizeInt, sizeDouble, arrInt, arrDouble, str); } // delete 하는 코드도 이전과 동일 bool DeleteStructDynamicArr(const StructSampleDynamic& sample) { delete[] sample.GetArrInt(); delete[] sample.GetArrDouble(); delete[] sample.GetArrChar(); return true; }
C++
복사
// C# 코드 int[] arrIntInput = { 1, 2, 3, 4, 5 }; double[] arrDoubleInput = { 10d, 20d, 30d }; StructSampleArr input = new StructSampleArr(sizeInt: arrIntInput.Length, sizeDouble: arrDoubleInput.Length, arrInt: arrIntInput, arrDouble: arrDoubleInput, str: "hello") // 내부에 array을 가진 구조체를 input으로 전달 StructSampleDynamic sample = NativeMethod.GetStructureDynamic(input : input); // 이하 코드는 이전과 동일 int[] arrInt = new int[sample.SizeInt]; Marshal.Copy(sample.PtrInt, arrInt, 0, sample.SizeInt); // arrInt 결과는 쓰레기 값 double[] arrDouble = new double[sample.SizeDouble]; Marshal.Copy(sample.PtrDouble, arrDouble, 0, sample.SizeDouble); // arrDouble 결과는 쓰레기 값 string str = Marshal.PtrToStringAnsi(sample.PtrStr); // str 결과는 "hello world" NativeMethod.DeleteStructDynamicArr(sample);
C#
복사
흥미롭게도 위 코드의 결과는 string은 제대로 처리가 되지만, int[], double[] 에는 쓰레기 값이 담겨 나온다.
이는 C#에서 넘긴 구조체 내부의 배열 값을 C++에서 제대로 받아오지 못하기 때문인데, —string에 대해서는 CLR이 별도로 처리를 해주기 때문에 문제 없이 처리가 된다— 이를 해결하려면 구조체 내부에서 배열을 선언하지 않고 IntPtr을 선언해서 처리해줘야 한다.
우선 앞서 정의한 구조체에서 내부 배열을 IntPtr 타입으로 수정한다.
// C# 코드 - int[], double[]를 모두 IntPtr 타입으로 수정한다. [StructLayout(LayoutKind.Sequential)] struct StructSampleArr { public StructSampleArr(int sizeInt, int sizeDouble, IntPtr ptrArrInt, IntPtr ptrArrDouble, string str) { this.SizeInt = sizeInt; this.SizeDouble = sizeDouble; this.PtrArrInt = ptrArrInt; this.PtrArrDouble = ptrArrDouble; this.Str = str; } public int SizeInt { get; private set; } public int SizeDouble { get; private set; } public IntPtr PtrArrInt { get; private set; } public IntPtr PtrArrDouble { get; private set; } public string Str { get; private set; } };
C#
복사
C++에서 받아온 IntPtr을 Marshal.Copy 해서 배열에 데이터를 가져온 과정을 거꾸로 해서 배열의 데이터를 IntPtr로 복사한 뒤 넘긴다.
// C# 코드 int[] arrIntInput = { 1, 2, 3, 4, 5 }; // Marshal.AllocCoTaskMem을 이용해서 IntPtr에 배열의 타입 x 크기만큼 메모리를 할당한다. // AllocCoTaskMem 외에 Marshal.AllocHGlobal을 사용할 수도 있다. 성능상 Marshal.AllocCoTaskMem이 낫다고 한다. IntPtr ptrArrInt = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * arrIntInput.Length); // 배열의 데이터를 IntPtr로 복사한다. Marshal.Copy(arrIntInput, 0, ptrArrInt, arrIntInput.Length); // double 배열도 마찬가지로 한다. double[] arrDoubleInput = { 10d, 20d, 30d }; IntPtr ptrArrDouble = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(double)) * arrDoubleInput.Length); Marshal.Copy(arrDoubleInput, 0, ptrArrDouble, arrDoubleInput.Length); StructSampleArr input = new StructSampleArr(sizeInt: arrIntInput.Length, sizeDouble: arrDoubleInput.Length, ptrArrInt: ptrArrInt, ptrArrDouble: ptrArrDouble, str: "hello") StructSampleDynamic sample = NativeMethod.GetStructureDynamic(input : input); // 이하 동일한 코드 내용 생략 // 결과는 의도한 대로 제대로 나온다. // ptr에 메모리를 직접 할당했으므로 해제도 직접 해줘야 한다. // 만일 Marshal.AllocHGlobal을 이용해서 메모리를 할당했다면 Marshal.FreeHGlobal를 이용해서 메모리 해제를 한다. Marshal.FreeCoTaskMem(ptrIntArr); Marshal.FreeCoTaskMem(ptrDoubleArr);
C#
복사

C#에서 C++로 내부에 구조체를 가진 구조체를 전달하고 return 받기

이번에는 내부에 구조체를 가진 구조체를 C#에서 C++로 전달하고 다시 return 받는 것을 살펴보자. 이것은 내부에 구조체를 가졌을 뿐 구조체를 전달하고 return 받는 것과 동일하다.
다음과 같이 내부에 구조체를 가진 구조체를 C++과 C#에 정의한다.
// C++ 코드 struct StructSampleGroup { public: StructSampleGroup(int size, StructSample source) : size(size), source(source) { } int GetSize() const { return this->size; } StructSample GetStruct() const { return this->source; } private: int size; StructSample source; };
C++
복사
// C# 코드 [StructLayout(LayoutKind.Sequential)] struct StructSampleGroup { public StructSampleGroup(int size, StructSample sample) { this.Size = size; this.Sample = sample; } public int Size { get; private set; } public StructSample Sample { get; private set; } };
C#
복사
사용법은 구조체를 사용한 것과 유사하고 결과도 예상 대로이다. —안쪽에 들어가는 구조체가 string이나 배열을 가진 경우에는 아래 로직으로는 동작하지 않는다.
// C++ 코드 StructSampleGroup GetStructureSampleGroup(const StructSampleGroup& input) { int sizeInt = input.GetSize() + 1; StructSample structInput = input.GetStruct(); StructSample structOuput(structInput.GetNumInt() * 2, structInput.GetNumDouble() * 0.5); return StructSampleGroup(sizeInt, structOuput); }
C++
복사
// C# 코드 StructSample input = new StructSample(numInt: 1, numDouble: 2d); StructSampleGroup inputGroup = new StructSampleGroup(size: 1, sample: input); StructSampleGroup outputGroup = NativeMethod.GetStructureSampleGroup(input: inputGroup); // 결과는 size 2, struct { numInt: 2, numDouble : 1 }
C++
복사

C#에서 C++로 내부에 구조체 배열을 가진 구조체를 전달하고 return 받기

마지막으로 내부에 구조체 배열을 가진 구조체를 전달하고 return 받는 것을 보자. 여튼 이것도 배열이기 때문에 IntPtr로 사용해야 한다.
구조체의 데이터를 IntPtr로 복사하는 부분만 차이가 있을 뿐 기본적인 개념은 배열을 사용하는 것과 유사하다.
우선 구조체 배열을 갖는 구조체를 C++과 C#에 정의하자.
// C++ 코드 struct StructSampleArrGroup { public: StructSampleArrGroup(int sizeStruct, StructSample* arrStruct) : sizeStruct(sizeStruct), arrStruct(arrStruct) { } int GetSizeStruct() const { return this->sizeStruct; } StructSample* GetArrStruct() const { return this->arrStruct; } private: int sizeStruct; StructSample* arrStruct; };
C++
복사
// C# 코드 [StructLayout(LayoutKind.Sequential)] struct StructSampleArrGroup { public StructSampleArrGroup(int size, IntPtr ptrStructArr) { this.Size = size; this.PtrStructArr = ptrStructArr; } public int Size { get; private set; } public IntPtr PtrStructArr { get; private set; } };
C#
복사
C++에서 C#의 구조체를 받아서 return 하는 코드는 다음과 같다.
// C++ 코드 StructSampleArrGroup GetStructureSampleArrGroup(const StructSampleArrGroup& input) { // input을 이용해서 return을 만든다. int sizeStruct = input.GetSizeStruct(); StructSample* structInput = input.GetArrStruct(); StructSample* structArr = new StructSample[sizeStruct]; for (int i = 0; i < sizeStruct; i++) { structArr[i] = StructSample(structInput[i].GetNumInt() * 2, structInput[i].GetNumDouble() * 0.5); } return StructSampleArrGroup(sizeStruct, structArr); } // C++ 내부에서 new를 하고 있으므로 delete 하는 코드도 만든다. bool DeleteStructSampleArr(const StructSampleArrGroup& input) { delete[] input.GetArrStruct(); return true; }
C++
복사
C#에서 C++로 구조체를 전달하기 위해 IntPtr로 복사하는 코드는 다음과 같다.
// C# 코드 int structSize = Marshal.SizeOf(typeof(StructSample)); // 전달할 데이터 List<StructSample> sources = new List<StructSample>(); sources.Add(new StructSample(numInt: 1, numDouble: 10d)); sources.Add(new StructSample(numInt: 2, numDouble: 20d)); sources.Add(new StructSample(numInt: 3, numDouble: 30d)); // IntPtr을 위한 메모리를 할당한다. IntPtr ptrStructArrInput = Marshal.AllocCoTaskMem(structSize * sources.Count); for (int i = 0; i < sources.Count; i++) { // StructureToPtr을 통해 구조체 데이터를 IntPtr로 복사한다. // 최초 할당한 위치로부터 구조체의 타입 x 배열의 index를 기준으로 복사를 반복한다. Marshal.StructureToPtr(sources[i], ptrStructArrInput + (structSize * i), true); } // 최종적으로 데이터를 복사한 IntPtr을 담는다. StructSampleArrGroup input = new StructSampleArrGroup(size: sources.Count, ptrStructArr: ptrStructArrInput); // C++ 함수를 수행하고 결과를 받는다. StructSampleArrGroup output = NativeMethod.GetStructureSampleArrGroup(input: input);
C#
복사
위와 같이 구조체 배열을 갖는 구조체를 C++로 전달한 뒤 C++ 에서 return 받으면 다음과 같이 구조체 데이터로 복사해 올 수 있다.
// C# 코드 // output에 담겨 있었던 구조체 배열의 길이 int size = output.Size; // IntPtr을 시작 주소로 사용한다. long address = output.PtrStructArr.ToInt64(); // output을 구조체로 변환해서 담을 list List<StructSample> outputs = new List<StructSample>(); for (int i = 0; i < size; i++) { // 시작 위치를 기준으로 구조체 타입의 크기 x 배열의 index를 기준으로 복사를 반복한다. // structSize는 위의 코드에서 선언한 Marshal.SizeOf(typeof(StructSample)) 이다. IntPtr ptrStructOutput = new IntPtr(address + (structSize * i)); // PtrToStructure을 통해 IntPtr의 데이터를 구조체로 복사해 온다. // 복사해 온 데이터의 타입을 모르기 때문에 강제로 StructSample 타입으로 형변환을 한다. StructSample output = (StructSample)Marshal.PtrToStructure(ptrStructOutput, typeof(StructSample)); // 구조체 데이터를 List에 담는다. outputs.Add(output); } // C++ 내부에서 new를 했으므로 delete 하는 C++ 함수 호출 NativeMethod.DeleteStructSampleArr(output); // C++ 에 넣기 위해 할당한 IntPtr 해제 Marshal.FreeCoTaskMem(ptrStructArrInput);
C#
복사

콜백 전달

경우에 따라 C++ 코드에서 C#의 코드를 호출해야 하는 경우가 있을 수 있다. 이런 콜백 함수도 다음과 같이 IntPtr을 이용해서 구현할 수 있다.
// delegate 선언 delegate bool EnumWC(IntPtr hwnd, IntPtr lParam); // C++에 delegate를 넘긴다. [DllImport("user32.dll")] static extern int EnumWindows(EnumWC lpEnumFunc, IntPtr lParam); // delegate 타입과 동일한 콜백 함수 선언 static bool OutputWindow(IntPtr hwnd, IntPtr lParam) { Console.WriteLine(hwnd.ToInt64()); return true; } public static void Main(string[] args) { // C++ 함수에 콜백함수를 넘긴다. EnumWindows(OutputWindow, IntPtr.Zero); }
C#
복사

참조 자료