C# 프로젝트에서 C++ DLL 연동하기 [기본/마샬링/콜백함수]

업데이트:

C# <-> C++ DLL 연동

개요

C#에서는 대부분 라이버러리를 누겟 패키지 관리자를 통해 바로 설치하여 사용하고 있다.
이렇게 보통 누겟을 통해 연동하거나 해봐야 프로젝트에 C# DLL을 참조를 걸어주며 사용한다.

그러나 C++로 개발된 DLL라이브러리를 사용할 경우 또한 많이 있다.
이 경우 늘 그랬듯이 프로젝트에 참조를 걸면 아래와 같은 에러가 발생한다.

~에 대한 참조를 추가할 수 없습니다. 파일이 액세스 가능한지, 어셈블리 또는 COM 구성요소가 올바른지 확인하십시오.

C#은 닷넷 기반 언어이기 때문에 C++로 만들어진 DLL과 연동이 되지 않는다.
그러면 일반적인 방법으로는 연동이 불가능하지만 닷넷 C#에서는 이런 DLL과 연동할 수 있는 런타임 라이브러리가 존재한다.

using System.Runtime.InteropServices;  
class HelloWorld
{
    [DllImport("TestLib.dll")]
    public static extern void GetNumber ();
}

위와 같이 선언 후 런타임시 해당 함수를 호출하면 DLL 내 함수가 호출된다.
그럼 자세히 알아보자.


DLL 연동 (기본)

기본적인 DLL 연동하는 예시 코드이다.

// C#
using System.Runtime.InteropServices;  
public static class DllExample
{
    [DllImport("TestLib.dll")]
    public static extern void GetNumber ();

    public static int Native_GetNumber()
    {
        int result = GetNumber();
        return result;
    }
}

var num = DllExample.Native_GetNumber()
Console.WriteLine("number:" + num);
// C++
int num = 100;
extern "C" __declspec(dllexport) int GetNumber() 
{
	return num;
}

위 C#코드를 먼저 보면 DLL호출을 선언한 함수를 바로 사용하는 것이 아니라 한번 더 함수로 묶여져 있다.
그리고 c++ 코드에서는 외부에서 사용할 함수를 선언해주어야 하는데 extern "C" __declspec(dllexport) 키워드를 앞에 넣어준다.

여기서 extern "C" 는 간단하게 C++ 코드를 C와 호환가능하도록 하는 규칙이다. DLL 생성/사용 시 일관성있는 규칙의 필요로 같이 선언해준다.

__declspec(dllexport) 는 DLL을 호출한 외부로의 Export를 위한 선언이다.


이제 파라미터를 입력해서 C++ DLL과 주고받아보자.

// C#
using System.Runtime.InteropServices;  
public static class DllExample
{
    [DllImport("TestLib.dll")]
    public static extern void AddNumber(int num1, int num2);

    public static int Native_AddNumber(int num1, int num2)
    {
        int result = AddNumber(300, 500);
        return result;
    }
}

var num = DllExample.Native_AddNumber();
Console.WriteLine("sum:" + num);
// C++
int num = 100;
extern "C" __declspec(dllexport) int AddNumber(int num1, int num2) 
{
    int sum = num1 + num1 + num;
	return sum;
}


DLL 연동 (마샬링)

마샬링이란 한 객체의 메모리에서의 표현방식을 저장 또한 전송에 적합한 다른 데이터 형식으로 변환하는 과정이다.

관련 포스팅은 아래 링크를 참고하면 된다.

  • https://hwanine.github.io/network/Marshalling/

C#과 C++ 데이터 연동시 데이터형을 아래의 표 내용과 같이 치환해주어야한다.

img

따라서 기본 자료형만 포함된 객체의 전송은 마샬링을 직접 구현 할 필요는 없고 치환만 잘 해주면된다.

아래 예시를 보자.

// C#
using System.Runtime.InteropServices;

public struct User
{
    public string name; 
    public int age;
}

public static class DllExample
{
    [DllImport("TestLib.dll")]
    public static extern User ChangeUser(User user);

    public static User Native_ChangeUser(User user)
    {
        User result = ChangeUser(user);
        return result;
    }
}

User user1 = new User();
user1.name = "홍길동";
user1.age = 20;

var changedUser = DllExample.Native_ChangeUser(user1);
Console.WriteLine("changedUser:" + changedUser.name + "," + changedUser.age);
// C++
typedef struct _User
{
	char* name;
	int	age;
} User;

const char* name = "홍길남";
int age = 28;
extern "C" __declspec(dllexport) int ChangeUser(User* user) 
{
    user->name = name;
    user->age = age;
	return sum;
}


직접 마샬링이 필요한 경우는 기본 자료형이 포함되어있지 않은 경우이다.
이 경우, C#에서 C++로 넘길 때에는 C++가 상대적 구 언어이기 때문에 C# 자료형으로 마샬링이 제한되기 때문에 C#에서 보낼때 자료형을 매치시켜서 보내주어야한다.
그러면 위에서 소개한 코드와 같이 Struct 타입으로 넘기면된다.

동일 자료형으로 구성된 Struct가 선언되어있지 않을 경우 아래와 같이 void 포인터로 주소를 받아서 형변환을 하던지 메모리 주소를 갖고 활용한다.

메모리 주소값으로 데이터통신을 하기 때문에 C#에서 받을때에서 IntPtr 자료형으로 값을 반환받아서 마샬링한다.

// C#
using System.Runtime.InteropServices;

public struct User
{
    public string name; 
    public int age;
}

public static class DllExample
{
    [DllImport("TestLib.dll")]
    public static extern IntPtr ChangeUser(User user);

    public static IntPtr Native_ChangeUser(User user)
    {
        IntPtr result = ChangeUser(user);
        User userValue = Marshal.PtrToStructure<User>(result);
        return userValue;
    }
}

User user1 = new User();
user1.name = "홍길동";
user1.age = 20;

var changedUser = DllExample.Native_ChangeUser(user1);
Console.WriteLine("changedUser:" + changedUser.name + "," + changedUser.age);
// C++
typedef struct _User
{
	char* name;
	int	age;
} User;

const char* name = "홍길남";
int age = 28;
extern "C" __declspec(dllexport) long ChangeUser(void* user_ptr) 
{
    // 형변환 시도
    (User*)user = user_ptr;
    user->age = 30;
	return &user_ptr;
}


DLL 연동 (콜백함수)

c++ DLL 연동을 하면서 콜백함수를 만들고 이벤트도 구현할 수 있다.
아래와 같이 콜백 함수를 보내고, void 포인터로 받아서 호출하는 방식으로 동작한다.

C#에서 delegate를 정의한 후, 마찬가지로 C++에서도 __stdcall 선언하여 콜백 함수를 정의한다.

// C#
using System.Runtime.InteropServices;

public struct User
{
    public string name; 
    public int age;
}

public static class DllExample
{
    public delegate void ChangedUserEvent(int isSuccess, IntPtr user);

    [DllImport("TestLib.dll")]
    public static extern void ChangeUser(User user, ChangedUserEvent callback);

    public static void Native_ChangeUser(User user, ChangedUserEvent callback)
    {
        ChangeUser(user, callback);
    }
}

public void CallbackFunction(int isSuccess, IntPtr value)
{
    User userValue = Marshal.PtrToStructure<User>(value);
    Console.WriteLine("changedUser:" + userValue.name + "," + userValue.age);
}

User user1 = new User();
user1.name = "홍길동";
user1.age = 20;
DllExample.Native_ChangeUser(user1, CallbackFunction);
// C++
typedef void __stdcall CallBackChangedUser(int isSuccess, void* user_ptr);

typedef struct _User
{
	char* name;
	int	age;
} User;

const char* name = "홍길남";
int age = 28;
extern "C" __declspec(dllexport) long ChangeUser(void* user_changed_callback) 
{
    // 형변환 시도
    (User*)user = user_ptr;
    user->age = 30;
    ((CallBackChangedUser*)user_changed_callback)(1, &user);
}


결론

이렇게 C#과 C++ 라이브러리간 연동이 가능하다.
연동에 사용되는데 필요한 선언 키워드는 다음과 같다.

Alt text

참고자료

태그:

카테고리:

업데이트:

댓글남기기