C# - C++과 연동
꼭 봐야 될 것
- http://sj21.wo.to/tt/483
- http://sj21.wo.to/tt/484
- http://blogs.msdn.com/junfeng/archive/2006/05/20/599434.aspx
How to: Marshal Structures Using C++ Interop How to: Marshal Embedded Pointers Using C++ Interop
C++
#include <msclr\marshal.h>
using namespace System;
using namespace msclr::interop;
int main(int argc, char** argv)
{
const char* x = "Mixing C++ and C#";
String^ y;
y = marshal_as<String^>(x);
return 0;
}
Overview of Marshaling in C++ http://msdn.microsoft.com/en-us/library/bb384865.aspx
C++에서 C#의 함수를 콜백으로 받아서 처리
C++
typedef void (__stdcall *PROGRESS)(int);
class CalcCPP
{
int Add(PROGRESS callBack, int a, int b)
{
int result = 0;
for(int i = 0; i < 100; ++i)
{
c = (a + b);
callBack(i);
}
return c;
}
}
C#에서 C++의 함수를 함수 콜백으로 사용 C++/CLR
C++/CLI
using namespace System;
using namespace System.Runtime.InteropServices;
namespace Test
{
public delegate void PROGRESS_CSHARP(int);
public ref class CalcWrapper
{
private:
CalcCPP* m_pCpp;
public:
CalcWrapper()
{m_pCpp = new CalcCPP()}
virtual ~CalcWrapper()
{if(m_pCpp){delete m_pCpp; m_pCpp = 0;}}
int Add(PROGRESS_CSHARP^ callBackFromCSharp,
int a, int b)
{
// GC를 할당한 뒤
GCHandle gch =
GCHandle::Alloc(callBackFromCSharp);
// delegate를 함수 포인터로 만듭니다.
IntPtr ip =
Marshal::GetFunctionPointerForDelegate(
callBackFromCSharp
);
// 그리고 형변환해서 대입
PROGRESS callBack =
static_cast<PROGRESS>(ip.ToPointer());
// 사용!
int result = m_pCpp->Add(callBack, a, b);
// 할당했던 GC를 비우면서 해제합니다
GC::Collect();
gch.Free();
return result;
}
}
}
C#에서 사용할 때는
C#
class TestClass
{
static void Progress(int pos)
{
System.Console.WriteLine(
"Currently : " + pos.ToString()
);
}
static void Main(string[] args)
{
Test.CalcWrapper cw = new Test.CalcWrapper();
int result = cw.Add(Progress, 1, 2);
System.Console.WriteLine(
"Result is " + result.ToString()
);
}
}
C/C++ 구조체와 함수를 C# 에서 사용하기
출처: http://hado.hadostudio.com/blog/category/DotNet
기존의 C/C++ 의 구조체와 함수들을 C# 의 클래스로 캡슐화하는 작업을 설명.
먼저, C/C++ 에서 DLL 프로젝트를 생성한다. (프로젝트 이름을 ‘BND.Native’ 라고 하면 출력물은 ‘BND.Native.dll’ 이 된다.)
1단계: C/C++ 구조체 정의 (MD5.h)
C++
typedef struct mD5Context
{
UINT state[4]; /* state (ABCD) */
UINT count[2]; /* number of bits, modulo 2^64 (lsb first) */
BYTE buffer[64]; /* input buffer */
}
MD5Context;
구조체 내부에 고정 크기의 배열이 정의되어 있다.
2단계: C/C++ 함수 정의 (MD5.cpp)
C++
void MD5Init(MD5Context * context)
{
...
}
void MD5Update(MD5Context * context, BYTE * input, UINT input_size)
{
...
}
void MD5Final(MD5Context * context, BYTE * output_digest)
{
...
}
C/C++ 언어답게(?) 포인터를 마구 사용하고 있다.
3단계: C/C++ 함수 선언 (MD5.h)
C++
extern "C" __declspec(dllexport) void MD5Init(MD5Context * context);
extern "C" __declspec(dllexport) void MD5Update(MD5Context * context, BYTE * input, UINT input_size);
extern "C" __declspec(dllexport) void MD5Final(MD5Context * context, BYTE * output_digest);
함수들을 DLL 외부로 노출한다.
이를 위해서 #include
4단계: 구조체 캡슐화
C#
public class MD5Calculator
{
// C/C++ 의 구조체와 동일한 형태가 되도록 정의합니다.
public struct MD5Context
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public UInt32[] state;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] public UInt32[] count;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)] public byte[] buffer;
}
// 클래스 내부에 구조체 인스턴스를 캡슐화합니다.
private MD5Context mContext = new MD5Context();
// 구조체를 초기화하는 코드를 생성자 내부에 작성합니다.
public MD5Calculator()
{
/* 따로 new 하지 않아도 이미 공간이 확보되어 있음.
mContext.state = new UInt32[4];
mContext.count = new UInt32[2];
mContext.buffer = new byte[64];
*/
}
C/C++ 에서 정의한 구조체와 동일한 구조체를 정의하고, 그 인스턴스를 정의하고, 초기화 코드를 작성하였다.
이 작업들을 편의상 하나의 클래스(MD5Calculator) 내에 캡슐화하였다.
5단계: 함수 참조 선언
C#
// C언어 함수들
[DllImport("BND.Native.dll")] extern public static void MD5Init(ref MD5Context ctx);
[DllImport("BND.Native.dll")] extern public static void MD5Update(ref MD5Context ctx, byte[] input, int input_size);
[DllImport("BND.Native.dll")] extern public static void MD5Final(ref MD5Context ctx, byte[] output_digest);
각종 포인터 파라미터가 위와 같이 매핑된다.
6단계: 함수 캡슐화
C#
public void Initial()
{
MD5Init(ref mContext);
}
public void Update(byte[] input, int input_size)
{
MD5Update(ref mContext, input, input_size);
}
public byte[] Final()
{
byte[] output_digest = new byte[16];
MD5Final(ref mContext, output_digest);
return output_digest;
}
구조체를 캡슐화 했듯이, 함수들도 캡슐화해 준다.
C#에서 C++ 함수로 포인터 배열 넘기기 혹시 저와 같은 고민을 하시는 분들이 보시고 조금이라도 도움이 되기를 바라며 1주일 동안 고생하며 스스로 찾아낸 답들을 올려보겠다.
C++
void CalOutputs(float* ivec);
float* GetOutput(void);
void GetOutput(float* ovec);
void TeachOnce(float* ivec, float* ovec, float Rate, float momentum);
위의 함수들은 예전에 작성된 C++ 클래스의 멤버 메쏘드이다.
모두 포인터 배열을 넘기고 받기 때문에 C#에서는 그대로 사용하기 어려운 면이 있어 C# wrapper class를 만들고 다음과 같이 메쏘드를 wrapping 했다.
배열 넘기기
C++
public void CalOutputs(float[] ivec)
{
unsafe
{
fixed(float* _ivec = ivec)
{
this.bpNet.CalOutputs(_ivec);
}
}
}
배열 리턴 값 받기
특히 이 부분이 어려웠는데, 배열을 바로 리턴 받을 방법은 없고 그렇다고 마셜링을 한다든가 하면 퍼포먼스에 문제가 있을 거 같아서 원본 C++ 클래스에는 float* GetOutput(void); 으로 되어 있던 메쏘드를 void GetOutput(float* ovec); 로 추가하여 wrapping 했다.
만일 소스코드를 바로 다룰 수가 없는 DLL 상태라든가 하면 DLL 과 C# 사이에 이 wrapping 해주는 메쏘드를 포함하는 C++ wrapper 클래스를 추가해주면 된다.
C#
public float[] GetOutput()
{
float[] result = new float[LENGTH];
unsafe
{
fixed(float* _result = result)
{
this.bpNet.GetOutput(_result);
}
}
return result;
}
같은 타입의 배열을 두 개 이상 넘길 때
C#
public void TeachOnce(float[] ivec, float[] ovec, float Rate, float momentum)
{
unsafe
{
fixed(float* _ivec = ivec, _ovec = ovec)
{
this.bpNet.TeachOnce(_ivec, _ovec, Rate, momentum);
}
}
}
이 글은 2019-05-19에 작성되었습니다.