출처 : cafe.naver.com/monodevelop
C++클래스를 C#에서 직접 래핑을 할지 C++/CLI 를 써야할지 고민중인 요즘인데....
일단 확보한 C++ => C#방안..
계속적으로 수정될것이고 그 양도 방대해질건데...
C#이던 C++/CLI이던 상당한 노가다가 필요하다고 보여져서 망설여진다..
Step1
wxWindows의 .NET용 포팅인 wx.NET의 소스 코드를 살펴보다가 재미있는 토픽을 한 가지 발견해서 글을 써올립니다.
C#은 System.Interop.DllImportAttribute 라는 속성을 사용해서 MS .NET에서는 DLL을, Mono의 경우 Win32 버전에서는 DLL을, Linux, Unix, Mac OS X 등의 OS에서는 Dynamic Library와의 Interop을 가능하게 해줍니다. Interop 할 수 있는 대상은 라이브러리 외부로 노출된 C 언어 스타일의 함수입니다. 하지만 C++은 C#에서 직접적으로 Interop을 성립시킬 수 없는데, 운영 체제와 컴파일러에 따라서 C++ 클래스를 참조하는 방식이 제각기 다르기 때문입니다.
지금 소개하는 방법은 매우 간단하면서도 생각보다 잘 작동하는 것 같습니다. 바로 C++의 멤버 메서드와 멤버 변수, 생성자와 소멸자에 대한 프록시 함수를 작성하는 방법입니다.
1. 시작하기에 앞서 간단한 C++ 클래스 정의하기
// TestClass.h에 정의하는 내용입니다.
class TestClass
{
public:
void TestFunction(int value);
int MyFunction(void);
private:
int m_myValue;
public:
int GetMyValue(void);
void SetMyValue(int value);
public:
TestClass(int value);
}
// TestClass.cpp에 정의하는 내용입니다.
#include <iostream>
void TestClass::TestFunction(int value)
{
stdout << value;
}
int TestClass::MyFunction(void)
{
return 123;
}
int TestClass::GetMyValue(void)
{
return this->m_myValue;
}
void TestClass::SetMyValue(int value)
{
this->m_myValue = value;
}
TestClass::TestClass(int value)
{
SetMyValue(value);
}
2. 생성자와 소멸자에 대한 프록시 함수 만들기
프록시 함수에서 생성자와 소멸자의 정의는 반드시 이루어져야 합니다. 모든 프록시 함수는 클래스의 인스턴스를 레퍼런스 형식으로 참조해야 하기 때문에 레퍼런스를 할당하고 제거하는 일을 하는 함수가 있어야합니다. C#에서는 이러한 일을 하기 위해서 복잡한 시스템 API (예를 들자면 malloc()과 free()와 유사한) 를 다시 참조해야 하므로 C++에서 대신 처리하는 것이 바람직합니다. 또한 생성자는 C++ 클래스를 바인딩하는 C# 클래스의 생성자에서 호출되어야 하며 소멸자는 System.IDisposable 인터페이스의 구현 메서드인 Dispose() 메서드 또는 C# 클래스의 소멸자에서 호출되어야 합니다.
프록시 함수는 라이브러리 외부로 노출되어야 하는 함수입니다. Win32에서는 DLL로 제작해야 하며 이 때에는 .def 파일을 선언하여 컴파일러에게 전달하거나 Visual C++ 컴파일러를 사용한다면 extern "C" __declspec(dllexport) 키워드를 사용해도 됩니다.
생성자 프록시 함수는 매개 변수로는 생성자의 매개 변수 그대로를 받습니다. 반환값은 포인터로 반환하면 됩니다. 만약 여러개의 생성자가 정의된 클래스라면 프록시 함수도 이름을 달리해서 여러개로 정의해주면 됩니다.
TestClass* TestClass_ctor(int value) // Creator
{
return new TestClass(value);
}
소멸자 프록시 함수는 다른 매개 변수는 받을 필요 없이 포인터 매개 변수 하나만 받습니다. 단, 메모리 액세스 위반을 방지하기 위하여 받은 인스턴스가 이미 정리된 인스턴스인지 확인하는 단계를 거쳐서 delete 연산자로 소멸시키면 됩니다.
void TestClass_dtor(TestClass* pInstance) // Destructor
{
if(pInstance != NULL)
delete pInstance;
pInstance = NULL;
}
일단 여기까지 글을 올립니다. 다음 글에서 멤버 함수와 멤버 변수에 관한 프록시를 만드는 방법을 소개하도록 하겠습니다.
Step2
지난 번 시간에는 C++ 클래스의 생성자와 소멸자에 대한 프록시 함수를 디자인 하는 것에 관해서 다루어보았습니다. 이번 시간에는 일반 멤버 함수, 정적 멤버 함수, 멤버 변수에 관한 사항을 다루어보도록 하겠습니다.
3. 일반적인 멤버 함수의 프록시 함수 만들기
멤버 함수는 크게 두 종류로 구분이 됩니다. 하나는 일반 멤버 함수이며 또 하나는 정적 멤버 함수입니다. 일반 멤버 함수는 클래스의 인스턴스가 생성된 이후에 호출할 수 있는 함수를 뜻합니다. 반면 정적 멤버 함수는 클래스의 이름을 네임스페이스로 취급하여 호출될 수 있는 함수를 뜻합니다. 쉽게 생각하시면 static 키워드를 붙인것과 붙이지 않은 것의 차이입니다. (C++의 static 키워드가 C#에서도 쓰이고 있으며, VB .NET에서는 Shared 키워드를 사용합니다.)
우선은 일반적인 멤버 함수에 관하여 생각해 보기로 하겠습니다.
void TestClass_TestFunction(TestClass* pInstance, int value)
{
if(pInstance)
pInstance->TestFunction(value);
}
int TestClass_MyFunction(TestClass* pInstance)
{
if(pInstance)
return pInstance->MyFunction();
return 0;
}
값을 반환하지 않는 멤버 함수의 프록시 함수를 만드는 것은 반환할 값에 관해 연연할 필요가 없으며 단지 받은 포인터가 NULL 참조인지 아닌지만을 확인해서 선택적으로 호출하기만 하면 되는 것입니다.
값을 받는 멤버 함수의 프록시 함수를 만드는 것은 정상적으로 호출이 이루어졌을 경우와 그렇지 않을 경우를 구분하여 값을 반환해야 합니다. 즉, 성공하면 메서드가 반환하는 값 그 자체를 반환해야 하며 호출에 실패할 경우 실패하였음을 공지하여야 합니다. 반환 값을 반환할 때에는 실행이 어디서 실패한 것인지를 분명히 구분해주어야 하므로 복잡한 로직을 설계하는 경우 신경써야 합니다.
4. 정적 멤버 함수의 프록시 함수 만들기
정적 멤버 함수는 클래스의 이름을 하나의 네임스페이스로 취급하는 함수이기 때문에 클래스의 인스턴스를 필요로 하지 않습니다. 단지 static으로 선언된 C++ 멤버 함수 하나만을 호출해주면 됩니다. 여기서는 앞에서 선언한 TestFunction과 MyFunction이 정적 멤버 함수라고 가정하고 예제를 보여드리도록 하겠습니다.
void TestClass_static_TestFunction(int value)
{
TestClass::TestFunction(value);
}
int TestClass_static_MyFunction()
{
return TestClass::MyFunction();
}
5. 멤버 변수에 관한 프록시 함수 만들기
멤버 변수에 관한 프록시 함수를 만들기 위해서는 몇 가지 사항을 더 고려해 봐야 합니다. 첫 번째 조건은 멤버 변수의 액세스 제한이 public 인지 아니면 private, protected와 같은 제약이 걸려있는지 여부입니다. 두 번째는 일반 멤버 변수인지 정적 변수인지 확인해야 합니다.
첫 번째 조건에서 후자의 경우에 해당된다면 클래스의 멤버 함수로 getter와 setter 멤버 함수를 도입해야 합니다. 보호된 멤버를 직접 액세스해서 값을 변경할 수는 없기 때문입니다. 그러나 전자의 경우에 해당된다면 getter와 setter 멤버 함수를 도입하지 말고 프록시 함수로 getter 함수와 setter 함수를 정의하시면 됩니다.
두 번째 조건에서 후자의 경우에 해당된다면 멤버 함수와 마찬가지로 getter와 setter 함수 모두에서 클래스의 인스턴스는 필요치 않습니다. 그러나 전자의 경우에 해당된다면 getter와 setter 함수 모두에서 클래스의 인스턴스가 필요합니다. getter에서는 멤버 변수의 값에 접근할 수 없었음을 알리는 실패 코드를 적절히 통지해야 하며 setter에서는 멤버 변수의 값에 접근할 수 없었음을 알리는 통지 코드나 출력용 매개 변수를 받아서 통지하는 것이 좋습니다. 저수준 코드에서는 C#의 try-catch 핸들러를 알지 못하기 때문입니다.
모두 네 가지 경우의 수가 존재합니다. 각각 살펴보도록 하겠습니다.
5.1. 만약 m_myValue가 public: 영역에 있으면서 일반 멤버 변수인 경우 getter와 setter 함수 구현
int TestClass_public_GetMyValue(TestClass* pInstance)
{
if(pInstance != NULL)
return pInstance->m_myValue;
else
return 0;
}
int TestClass_public_SetMyValue(TestClass* pInstance, int value)
{
if(pInstance != NULL)
{
pInstance->m_myValue = value;
return 1; // 성공했음을 알리는 표현
}
return 0; // 액세스에 실패했음을 알리는 표현
}
5.2. 만약 m_myValue가 public: 영역에 있으면서 정적 멤버 변수인 경우 getter와 setter 함수 구현
int TestClass_public_static_GetMyValue()
{
return TestClass::m_myValue;
}
// 정적 멤버 변수는 특별한 상황이 아닌 이상 실패하는 경우는 없습니다.
// 굳이 정확성을 추구하자면 C++의 Structured Exception Handling을 사용해도 좋지만
// 복잡한 로직을 사용하지 않을 경우 성능상의 손해가 있을 수 있습니다.
void TestClass_public_static_SetMyValue(int value)
{
TestClass::m_myValue = value;
}
5.3. 만약 m_myValue가 public:이 아닌 영역에 있으면서 일반 멤버 변수인 경우 getter와 setter 함수 구현 (단, 클래스의 멤버 함수에 있는 getter와 setter 함수는 public: 이어야 합니다.)
int TestClass_private_GetMyValue()
{
if(pInstance != NULL)
return pInstance->GetMyValue();
else
return 0;
}
int TestClass_private_SetMyValue(int value)
{
if(pInstance != NULL)
{
pInstance->SetMyValue(value);
return 1; // 성공했음을 알리는 표현
}
return 0; // 액세스에 실패했음을 알리는 표현
}
5.4. 만약 m_myValue가 public:이 아닌 영역에 있으면서 정적 멤버 변수인 경우 getter와 setter 함수 구현 (단, 클래스의 멤버 함수에 있는 getter와 setter 함수는 public: 이면서 정적 멤버 함수여야 합니다.)
int TestClass_private_static_GetMyValue()
{
return TestClass::GetMyValue();
}
void TestClass_private_static_SetMyValue(int value)
{
TestClass::SetMyValue(value);
}
오늘 시간에는 세 가지 사항을 살펴봤습니다. 다음 시간에는 마지막으로 C#과의 연동 방법에 관하여 살펴보도록 하겠습니다.
오늘 시간에 강의한 내용이 프록시 함수를 설계하는 패턴의 전부는 아닙니다. 얼마든지 새로운 방식을 사용하여 입맛에 맞게 고치실 수 있음을 인지하시고 활용해 주십시오. 그리고 이 프록시 함수는 포인터를 활용해서 구현된다는 점을 잊으시면 안됩니다.
긴 강좌를 봐주셔서 감사합니다. 행복한 하루 되세요. ^^
Step3
이번 시간에는 지난 번 시간을 이어 마지막으로 C++에서 제작한 클래스를 바인딩하는 C# 클래스를 만들어보도록 하겠습니다.
6. 프록시 함수의 완성본 만들기
#ifdef MSC_VER // VC++ 컴파일러를 위한 Export 구문 설정 (DLL 버전)
#define EXPORT extern "C" __declspec(dllexport)
#else // VC++ 이외의 컴파일러를 위한 Export 구문 설정 (적절한 설정이 필요할 수도 있습니다.)
#define EXPORT
#endif
EXPORT TestClass* TestClass_ctor(int value)
{
return new TestClass(value);
}
EXPORT void TestClass_dtor(TestClass* pInstance)
{
if(pInstance != NULL)
delete pInstance;
pInstance = NULL;
}
EXPORT void TestClass_TestFunction(TestClass* pInstance, int value)
{
if(pInstance != NULL)
pInstance->TestFunction(value);
}
EXPORT int TestClass_MyFunction(TestClass* pInstance)
{
if(pInstance != NULL)
return pInstance->MyFunction();
return 0;
}
EXPORT int TestClass_GetMyValue(TestClass* pInstance)
{
if(pInstance != NULL)
return pInstance->GetMyValue();
return 0;
}
EXPORT int TestClass_SetMyValue(TestClass* pInstance, int value)
{
if(pInstance != NULL)
{
pInstance->SetMyValue(value);
return 1;
}
return 0;
}
7. C#에서 라이브러리 참조하기
Windows의 C++ 컴파일러를 사용해서 컴파일할 경우 DLL로 컴파일시키며, Linux/Unix/Mac OS X 등의 C++ 컴파일러를 사용해서 컴파일할 경우 Dynamic Library (확장자는 대개 so 파일)로 컴파일시킵니다. 그리고 Windows에서는 %windir%\system32 디렉터리에, Linux에서는 /usr/lib과 같은 라이브러리 파일을 집결시키는 디렉터리에 만들어진 라이브러리를 배치합니다. 혹은 C# 프로그램 파일이 있는 곳에 같이 배치해도 됩니다.
using System;
using System.Runtime.InteropService;
public sealed class TestClassBinding
{
// 모든 프록시 함수는 이 변수를 참조합니다.
private IntPtr rawValue = null;
[DllImport("라이브러리 파일 이름")]
private static extern IntPtr TestClass_ctor(int value);
[DllImport("라이브러리 파일 이름")]
private static extern void TestClass_dtor(IntPtr instance);
[DllImport("라이브러리 파일 이름")]
private static extern void TestClass_TestFunction(IntPtr instance, int value);
[DllImport("라이브러리 파일 이름")]
private static extern int TestClass_MyFunction(IntPtr instance);
[DllImport("라이브러리 파일 이름")]
private static extern int TestClass_GetMyValue(IntPtr instance);
[DllImport("라이브러리 파일 이름")]
private static extern int TestClass_SetMyValue(IntPtr instance, int value);
public TestClassBinding(int value)
{
this->rawValue = TestClass_ctor(value);
}
~TestClassBinding()
{
TestClass_dtor(this->rawValue);
}
public void TestFunction(int value)
{
TestClass_TestFunction(this->rawValue, value);
}
public int MyFunction()
{
return TestClass_MyFunction(this->rawValue);
}
public int MyValue
{
get
{
return TestClass_GetMyValue(this->rawValue);
}
set
{
try
{
if(TestClass_SetMyValue(this->rawValue, value) == 0)
throw new Exception("할당되지 않은 포인터를 사용하려 했습니다.");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
public int GetMyValue()
{
return TestClass_GetMyValue(this->rawValue);
}
public void SetMyValue(int value)
{
try
{
if(TestClass_SetMyValue(this->rawValue, value) == 0)
throw new Exception("할당되지 않은 포인터를 사용하려 했습니다.");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
이것으로 세 단계에 걸친 강좌를 모두 끝내도록 하겠습니다. 다음 시간에는 또 다른 토픽을 가지고 강좌를 올리도록 하겠습니다. 많은 도움이 되셨기를 바랍니다. ^^
Posted by 자갈공명