P/Invoke의 이해
P/Invoke
**P/Invoke(Platform Invocation)**는 DLL로 구현된 Unmanaged Code를 C#과 같은 Managed Code에서 사용할 수 있도록 연동과 관련된 기능을 제공한다.
System.Runtime.InteropServices
[DllImport("example.dll", EntryPoint = "printInfo", CallingConvention = CallingConvention.Cdecl)]
static extern void printInfo();
- **DllImport()**에 DLL 파일명과 속성을 지정하고
static
과extern
키워드를 사용하여 Method를 정의한다. - EntryPoint : DLL에 포함된 Method 중 호출할 Method의 이름을 지정한다. 이 속성을 생략할 경우 Method의 이름을 그래도 사용하고, 속성을 사용하면 다른 이름으로 Method를 사용할 수 있다.
- CallingConvention : Method를 호출할 때 매개변수를 전달하는 방식이나 Method를 호출한 뒤 Stack Frame 정리에 관한 규칙을 지정한다.
예제
// example.dll
#include <stdio.h>
extern "C"
{
__declspec(dllexport) void printInfo()
{
printf("P/Invoke Example\n");
}
__declspec(dllexport) int computeArea(int width, int height)
{
return width * height;
}
}
// C#
[DllImport("example.dll", EntryPoint = "printInfo")] // 사용자 정의 DLL
static extern void exampleInfo();
[DllImport("example.dll", CallingConvention = CallingConvention.Cdecl)]
static extern int computeArea(int width, int height);
[DllImport("user32.dll", CallingConvention = CallingConvention.Cdecl)] // WinAPI 관련 DLL
static extern int wsprintf([Out] StringBuilder lpOut, string lpFmt, int x);
static void Main()
{
exampleInfo(); // example.dll
int area = computeArea(3, 5); // example.dll
StringBuilder buffer = new StringBuilder();
wsprintf(buffer, "Area : %d", area); // user32.dll
Console.WriteLine(buffer.ToString());
}
출력 결과 :
P/Invoke Example
Area : 15
Marshaling
Marshaling은 한 언어로 작성된 프로그램의 표현방식을 다른 언어에 적합하게 다른 형식으로 변환하는 과정을 말한다.
- MarshalAs을 사용하여 Method의 매개변수나 반환 값을 Marshaling할 수 있다.
[DllImport("example.dll", CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I4)]
static extern int stringLength(
[MarshalAs(UnmanagedType.LPStr)]
string str);
- Unmanaged Method에서 사용되는 구조체 및 클래스의 Field를 Marshaling할 수 있다.
// example.dll
typedef struct Data
{
char name[10];
int value[5];
int count;
} Data;
// C#
[StructLayout(LayoutKind.Sequential)]
public struct Data
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 10)]
public string name;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
public int[] value;
public int count;
}
[DllImport("example.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void checkData(Data data);
static void Main()
{
Data data = new Data();
/* 생략 */
checkData(data);
}
Marshal Method
멤버 | 설명 |
---|---|
Marshal.AllocHGlobal() | Unmanaged 메모리에 지정된 크기의 공간을 할당 |
Marshal.FreeHGlobal() | Unmanaged 메모리에 할당된 공간을 해제 |
Marshal.ReadInt32() | Unmanaged 메모리에서 32bit 부호 있는 정수를 읽음 |
Marshal.ReadByte() | Unmanaged 메모리에서 byte 형식의 변수를 읽음 |
Marshal.Copy() | Unmanaged 메모리의 Pointer를 Managed 형식의 배열에 또는 그 반대로 값을 복사 |
Marshal.SizeOf() | Unmanaged 형식의 변수 크기를 byte 단위로 반환 |
예제
int intSize = 4;
IntPtr unmanagedArray = Marshal.AllocHGlobal(5 * intSize);
int[] managedArray = { 1, 2, 3, 4, 5 };
Marshal.Copy(managedArray, 0, unmanagedArray, managedArray.Length); // managedArray -> unmanagedArray
for (int i = 0; i < 5; i++)
{
int result = Marshal.ReadInt32(unmanagedArray, i * intSize);
Console.WriteLine(result);
}
int ptrSize = Marshal.SizeOf(unmanagedArray); // 4
int[] managedArrayCopy = new int[managedArray.Length];
Marshal.Copy(unmanagedArray, managedArrayCopy, 0, 5 * ptrSize); // unmanagedArray -> managedArrayCopy
Marshal.FreeHGlobal(unmanagedArray);
예제 분석
U8Conv.StringFromUtf8Ptr()
public static string StringFromUtf8Ptr(IntPtr ptr) // ptr is null-terminated
{
if (ptr == IntPtr.Zero)
return "";
int len = 0;
while (Marshal.ReadByte(ptr, len) != 0) len++;
if (len == 0)
return "";
byte[] array = new byte[len];
Marshal.Copy(ptr, array, 0, len);
return Encoding.UTF8.GetString(array);
}
-
IntPtr
은 Pointer를 나타내는 형식이다. 매개변수를IntPtr
형식인 ptr로 받는다. - ptr을
byte
단위로 읽어서 0이 되기 전까지 len을 증가시켜 문자열의 길이를 구한다. - Unmanaged Pointer(ptr)의 값을 Managed byte 배열(array)에 지정한 길이만큼 복사한다.
- ptr에 저장된 UTF-8 방식의 문자열을 byte 배열인 array에 저장한 것이다. 이 문자열을 string 형식으로 Encoding 후 반환한다.
U8Conv.Utf8BytesFromString()
public static byte[] Utf8BytesFromString(string s)
{
var cnt = Encoding.UTF8.GetMaxByteCount(s.Length);
byte[] buf = new byte[cnt + 1];
Encoding.UTF8.GetBytes(s, 0, s.Length, buf, 0);
return buf;
}
- 문자열 길이에 따라 UTF-8 방식으로 Encoding하여 나올 수 있는 최대 크기를 cnt에 저장한다.
- Unmanaged 문자열은 배열의 끝 요소에 0('\0')이 저장되어야 하므로 cnt보다 1 증가한 크기의 byte 배열인 buf를 생성한다.
- string 형식의 s 변수를 UTF-8 방식으로 Encoding한 후 byte 배열인 buf에 저장하여 반환한다.
Kai.DisplayStr()
if (bufsize == 0)
bufsize = Kai.OneRowSize;
IntPtr buf = IntPtr.Zero;
- bufsize가 0이면 Kzi.OneRowSize(1024)로 초기화한다.
- buf를 IntPtr.Zero의 값인 0으로 초기화한다.
try
{
buf = Marshal.AllocHGlobal(bufsize + 1);
kai_display_str(id, buf, bufsize);
return U8Conv.StringFromUtf8Ptr(buf);
}
- Unmanaged 메모리에 bufsize+1 크기만큼의 buf를 할당한다.
- kai_display_str()에서는 해당 kai ID(id)의 데이터를 buf에 저장한다.
- buf에 저장된 문자열은 UTF-8 방식이므로 U8Conv.StringFromUtf8Ptr()을 사용해 string 형식으로 변환 후 반환한다.
finally
{
Marshal.FreeHGlobal(buf);
}
- return 후 Unmanaged 메모리에 할당된 buf를 해제한다.
Kai.CellByName()
[DllImport("kai_d.dll", EntryPoint = "kai_cell_byname", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr kai_cell_byname(int id, byte[] col_name, int row);
- 'kai_d.dll'에 포함된 kai_cell_byname()를 사용하기 위해 DllImport로 정의한 것이다.
public static string CellByName(int id, string col_name, int row)
{
var col = U8Conv.Utf8BytesFromString(col_name);
var ptr = kai_cell_byname(id, col, row);
return U8Conv.StringFromUtf8Ptr(ptr);
}
- string 형식의 col_name을 U8Conv.Utf8BytesFromString()에서 UTF-8 방식으로 Encoding한 후 byte 배열로 반환하여 col에 저장한다.
- kai_cell_byname()를 사용하여 해당 Kai ID(id)에서 byte 배열(col)의 row 번째 내용을 반환하여 ptr에 저장한다.
- ptr에 저장된 문자열은 UTF-8 방식이므로 U8Conv.StringFromUtf8Ptr()을 사용해 string 형식으로 변환 후 반환한다.
Kai.row_howmany()
[DllImport("kai_d.dll", EntryPoint = "kai_row_howmany", CallingConvention = CallingConvention.Cdecl)]
public static extern int row_howmany(int id, out int rows);
- 'kai_d.dll'에 포함된 kai_row_howmany()를 row_howmany()로 사용하기 위해 EntryPoint 속성을 지정하여 DllImport로 정의한 것이다.
// kai_d.dll
int kai_row_howmany(int id, int *howmany) { /* 내용 */ }
- kai_row_howmany()은 해당 Kai ID(id)에서 howmany의 row 수를 세어 반환하는 Method이다.
- kai_row_howmany()의 선언 부분에는 매개변수 howmamy를 Pointer로 받아온다. 이를 C#에서 사용하기 위해 정의할 때
out
키워드를 사용하여 매개변수를 Call By Reference로 넘겨준다.
Metic.init(), Metic.Init()
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void Connection(metic_status status, int sv_info_kai);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void LoginRsp(int success, int info_kai);
- Unmanaged Function Pointer를 사용하기 위해 Managed Code의 Delegate로 정의한 것이다.
[DllImport("metic_d.dll", EntryPoint = "metic_init", CallingConvention = CallingConvention.Cdecl)]
public static extern int init(byte[] server_ip, ushort server_port, int init_conn_try_time, Connection conn_cb, LoginRsp login_rsp_cb);
- 'metic_d.dll'에 포함된 metic_init()을 init()으로 사용하기 위해 EntryPoint 속성을 지정하여 정의한 것이다.
public static int Init(string serverIP, ushort serverPort, int connTryTime, Connection connChange, LoginRsp loginRsp)
{
var server_ip = U8Conv.Utf8BytesFromString(serverIP);
return init(server_ip, serverPort, connTryTime, connChange, loginRsp);
}
- metic을 초기화하는 데 필요한 정보를 매개변수로 받아서 init()을 호출시킨다.
// metic_d.dll
void conn_cb(enum metic_status status, int sv_info_kai) { /* 내용 */ }
void login_rsp_cb(int success, int info_kai) { /* 내용 */ }
int metic_init(char *server_ip, unsigned short server_port, int init_conn_try_time,
void (*conn_cb)(enum metic_status status, int sv_info_kai),
void (*login_rsp_cb)(int success, int info_kai)) { /* 내용 */ }
- metic_init() 매개변수로 받은 정보를 이용해 metic을 초기화하는 Method이다.
- 매개변수에서 char 형식의 Pointer로 선언된 server_ip를 C#에서 사용하기 위해 같은 크기인 byte 형식의 배열로 대체한다.
- Function Pointer로 선언된 conn_cb와 login_rsp_cb는 Delegate로 선언된 Connection과 LoginRsp로 대체하여 사용한다. 이와 같은 형식의 Method를 매개변수로 받아서 사용할 수 있다.