C#에서 C++ DLL을 호출해야 하는 경우
C#으로 클라이언트 애플리케이션을 개발하다 보면 종종 네이티브 C++ 라이브러리를 사용해야 하는 상황이 발생합니다. 이때 P/Invoke(Platform Invoke) 기능을 활용하면 C# 프로그램에서 C++로 작성된 DLL을 호출할 수 있습니다. 본 문서에서는 Visual Studio 2022 환경에서 C++ DLL을 생성하고 C#에서 호출하는 전체 과정을 단계별로 설명하겠습니다.
1. C++ DLL 프로젝트 생성
먼저 Visual Studio 2022에서 C++ DLL 프로젝트를 생성하는 과정입니다.
1.1 프로젝트 설정
Windows Desktop Wizard를 선택하고 프로젝트를 생성합니다. 프로젝트 이름은 예시로 NativeMath를 사용하겠습니다. 프로젝트 유형은 Dynamic Library(.dll)로 선택하고 Export Symbols 옵션을 체크합니다.
1.2 헤더 파일 작성
DLL에서 내보낼 함수와 클래스를 선언합니다. C++ 클래스를 C#에서 직접 사용하려면 언어적인 차이로 인해 직접 호출이 불가능합니다. 따라서 C++ 클래스를 감싸는 외부 함수들을 추가로 작성해야 합니다.
// NativeMath.h
#ifdef NATIVEMATH_EXPORTS
#define NATIVEMATH_API __declspec(dllexport)
#else
#define NATIVEMATH_API __declspec(dllimport)
#endif
class NATIVEMATH_API CMathHelper {
public:
CMathHelper();
int Multiply(int a, int b);
int Divide(int a, int b);
};
// C#에서 사용하기 위한 래퍼 함수들
extern "C" {
NATIVEMATH_API CMathHelper* CreateMathInstance();
NATIVEMATH_API void DestroyMathInstance(CMathHelper* ptr);
NATIVEMATH_API int ExecuteMultiply(CMathHelper* ptr, int x, int y);
NATIVEMATH_API int ExecuteDivide(CMathHelper* ptr, int x, int y);
}
중요: extern "C"를 사용하지 않으면 C++ 컴파일러가 함수 이름을修饰装飾하여 복잡한 형태로 변경합니다. 예를 들어 Multiply 함수가编译 후 ?Multiply@@YAHHH@Z 같은 형태로 변경될 수 있습니다. C#侧的DllImport는装飾된 이름을 인식하지 못하므로 반드시 C 링크 방식을 명시해야 합니다.
1.3 소스 파일 작성
// NativeMath.cpp
#include "framework.h"
#include "NativeMath.h"
CMathHelper::CMathHelper() {
}
int CMathHelper::Multiply(int a, int b) {
return a * b;
}
int CMathHelper::Divide(int a, int b) {
if (b != 0) {
return a / b;
}
return 0;
}
NATIVEMATH_API CMathHelper* CreateMathInstance() {
return new CMathHelper();
}
NATIVEMATH_API void DestroyMathInstance(CMathHelper* ptr) {
delete ptr;
}
NATIVEMATH_API int ExecuteMultiply(CMathHelper* ptr, int x, int y) {
return ptr->Multiply(x, y);
}
NATIVEMATH_API int ExecuteDivide(CMathHelper* ptr, int x, int y) {
return ptr->Divide(x, y);
}
프로젝트를 빌드하면 NativeMath.dll 파일이 생성됩니다. 이 DLL을 C# 프로젝트의 실행 파일이 있는 디렉토리에 복사해야 합니다.
2. C#에서 C++ DLL 호출
C#에서는 DllImport 특성을 사용하여 네이티브 DLL의 함수를 선언합니다.
using System;
using System.Runtime.InteropServices;
namespace ManagedCaller
{
internal class Program
{
private static IntPtr mathInstance;
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateMathInstance();
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void DestroyMathInstance(IntPtr ptr);
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int ExecuteMultiply(IntPtr ptr, int a, int b);
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int ExecuteDivide(IntPtr ptr, int a, int b);
static void Main(string[] args)
{
Console.WriteLine("C#에서 C++ DLL 호출 테스트");
// C++ 객체 생성
mathInstance = CreateMathInstance();
// Multiply 함수 호출
int multiplyResult = ExecuteMultiply(mathInstance, 5, 3);
Console.WriteLine($"5 * 3 = {multiplyResult}");
// Divide 함수 호출
int divideResult = ExecuteDivide(mathInstance, 20, 4);
Console.WriteLine($"20 / 4 = {divideResult}");
// 메모리 정리 - 중요: 네이티브 메모리 수동 해제 필요
DestroyMathInstance(mathInstance);
}
}
}
2.1 호출 규약 확인
C++ 함수의 호출 규약과 C#의 CallingConvention이 일치해야 합니다. 대부분의 C++ DLL은 기본적으로 cdecl 규약을 사용하므로 위 예제와 같이 명시적으로 지정합니다. 만약 stdcall을 사용하는 경우 CallingConvention.StdCall로 변경해야 합니다.
2.2 래퍼 클래스 활용
코드의 가독성과 메모리 관리便捷성을 위해 C# 래퍼 클래스를 구현하는 것이 좋습니다.
public class MathWrapper : IDisposable
{
private IntPtr nativeInstance;
private bool disposed = false;
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateMathInstance();
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void DestroyMathInstance(IntPtr ptr);
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int ExecuteMultiply(IntPtr ptr, int a, int b);
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int ExecuteDivide(IntPtr ptr, int a, int b);
public MathWrapper()
{
nativeInstance = CreateMathInstance();
}
public int Multiply(int a, int b)
{
return ExecuteMultiply(nativeInstance, a, b);
}
public int Divide(int a, int b)
{
return ExecuteDivide(nativeInstance, a, b);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// 관리되는 리소스 정리
}
if (nativeInstance != IntPtr.Zero)
{
DestroyMathInstance(nativeInstance);
nativeInstance = IntPtr.Zero;
}
disposed = true;
}
}
~MathWrapper()
{
Dispose(false);
}
}
래퍼 클래스 사용:
using (MathWrapper math = new MathWrapper())
{
Console.WriteLine($"곱셈: {math.Multiply(7, 8)}");
Console.WriteLine($"나눗셈: {math.Divide(100, 5)}");
}
3. 데이터 타입 매핑
C++와 C# 사이의 타입 상호 변환을 올바르게 처리해야 합니다.
3.1 기본 타입 매핑
| C++ 타입 | C# 타입 |
|---|---|
| bool | bool |
| char | byte |
| short | short |
| int | int |
| long | int (32비트) / long (64비트) |
| float | float |
| double | double |
| char* | string |
| wchar_t* | string |
3.2 구조체 처리
C++ 구조체를 C#에서 사용하려면 StructLayout 특성을 적용합니다.
// C++ 구조체
struct UserInfo {
char* name;
int id;
double score;
};
// C# 매핑
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public class UserInfo
{
[MarshalAs(UnmanagedType.LPStr)]
public string name;
public int id;
public double score;
}
3.3 포인터 타입 처리
| C++ 포인터 | C# 표현 |
|---|---|
| int* | IntPtr 또는 ref int |
| char* | string (DllImport가 자동 변환) |
| void* | IntPtr |
| struct* | ref StructName 또는 IntPtr |
IntPtr을 사용하는 경우 Marshal 클래스의 메서드를 활용하여 메모리를 직접 관리해야 합니다.
3.4 함수 포인터 (콜백)
C++에서 함수 포인터를 사용하는 경우 C# 델리게이트로 변환합니다.
// C++ 콜백 타입
typedef void (*CallbackFunc)(int result);
// C# 델리게이트
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void CallbackDelegate(int result);
4. 주의사항
- 빌드 구성: C++ DLL과 C# 프로젝트의 플랫폼(x86/x64)이 일치해야 합니다. Debug/Release 구성도 맞춰야 합니다.
- 메모리 관리: C++에서 할당된 메모리는 C++에서 해제해야 합니다. C# 가비지 컬렉터가 네이티브 메모리를 관리하지 못합니다.
- DLL 경로: DLL이 실행 파일과 같은 디렉토리에 있거나 시스템 PATH에 포함되어야 합니다.
- 권한: 애플리케이션이 DLL을 로드할 수 있는 적절한 권한을 가지고 있어야 합니다.
이제 C#에서 C++ DLL을 호출하는 기본적인 방법을 알아보았습니다. 실제 프로젝트에서는 구조체의 크기, 배열 처리, 예외 처리 등 더 복잡한 상황들을 고려해야 할 수 있습니다.