P/Invoke(플랫폼 호출)
P/Invoke는 관리 코드에서 비관리형 라이브러리의 구조체, 콜백 및 함수에 액세스할 수 있는 기술입니다. P/Invoke API는 대부분 System
및 System.Runtime.InteropServices
라는 두 네임스페이스에 포함되어 있습니다. 이러한 두 네임스페이스를 사용하면 기본 구성 요소와 통신하는 방법을 설명하는 도구가 제공됩니다.
관리 코드에서 관리되지 않는 함수를 호출하는 가장 일반적인 예에서 시작하겠습니다. 명령줄 애플리케이션에서 메시지 상자를 표시하겠습니다.
using System;
using System.Runtime.InteropServices;
public partial class Program
{
// Import user32.dll (containing the function we need) and define
// the method corresponding to the native function.
[LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
private static partial int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
public static void Main(string[] args)
{
// Invoke the function as a regular managed method.
MessageBox(IntPtr.Zero, "Command-line message box", "Attention!", 0);
}
}
앞의 예제는 간단하지만 관리 코드에서 관리되지 않는 함수를 호출하는 데 필요한 사항을 보여 줍니다. 예를 단계별로 살펴보겠습니다.
- 줄 #2에는 필요한 모든 항목을 포함하는 네임스페이스에 대한
System.Runtime.InteropServices
지시문이 표시됩니다using
. - 줄 #8에서는 LibraryImportAttribute 특성을 도입합니다. 이 특성은 관리되지 않는 이진을 로드해야 함을 런타임에 알려 줍니다. 전달된 문자열은 대상 함수를 포함하는 관리되지 않는 이진 파일입니다. 또한 문자열을 마샬링하는 데 사용할 인코딩을 지정합니다. 마지막으로 이 함수가 SetLastError를 호출하고 사용자가 Marshal.GetLastPInvokeError()을 통해 검색할 수 있게 런타임이 이 오류 코드를 캡처하도록 지정합니다.
- 줄 #9은 P/Invoke 작업의 핵심입니다. 관리되지 않는 메서드와 동일한 시그니처가 있는 관리되는 메서드를 정의합니다. 이 선언에서는
LibraryImport
특성과partial
키워드를 사용하여 관리되지 않는 라이브러리를 호출할 코드를 생성하도록 컴파일러 확장에 지시합니다.- 생성된 코드 내에서 .NET 7 이전에는
DllImport
가 사용되었습니다. 이 선언은extern
키워드를 사용하여 외부 메서드로 호출할 때 런타임이DllImport
특성에 지정된 관리되지 않는 이진 파일에서 이를 찾아야 함을 런타임에 나타냅니다.
- 생성된 코드 내에서 .NET 7 이전에는
이 예의 나머지 부분에서는 다른 관리되는 메서드와 마찬가지로 메서드를 호출합니다.
macOS에 대한 샘플도 이와 비슷합니다. macOS에서는 다른 동적 라이브러리 명명 체계를 사용하므로 LibraryImport
특성의 라이브러리 이름을 변경해야 합니다. 다음 샘플에서는 getpid(2)
함수를 사용하여 애플리케이션의 프로세스 ID를 가져오고 콘솔에 출력합니다.
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Import the libSystem shared library and define the method
// corresponding to the native function.
[LibraryImport("libSystem.dylib")]
private static partial int getpid();
public static void Main(string[] args)
{
// Invoke the function and get the process ID.
int pid = getpid();
Console.WriteLine(pid);
}
}
}
Linux에서도 이와 비슷합니다. getpid(2)
는 표준 POSIX 시스템 호출이기 때문에 함수 이름이 같습니다.
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Import the libc shared library and define the method
// corresponding to the native function.
[LibraryImport("libc.so.6")]
private static partial int getpid();
public static void Main(string[] args)
{
// Invoke the function and get the process ID.
int pid = getpid();
Console.WriteLine(pid);
}
}
}
비관리 코드에서 관리 코드 호출
런타임에서 양방향 통신을 허용하므로 함수 포인터를 사용하여 네이티브 함수에서 관리 코드로 콜백할 수 있습니다. 관리 코드에서 함수 포인터와 가장 가까운 것은 대리자이므로, 대리자를 사용하여 네이티브 코드에서 관리 코드로의 콜백을 허용합니다.
이 기능을 사용하는 방법은 앞에서 설명한 관리 코드에서 네이티브 코드로의 프로세스와 비슷합니다. 지정된 콜백에 대해 시그니처와 일치하는 대리자를 정의하고 외부 메서드에 전달합니다. 다른 모든 작업은 런타임에서 수행합니다.
using System;
using System.Runtime.InteropServices;
namespace ConsoleApplication1
{
public static partial class Program
{
// Define a delegate that corresponds to the unmanaged function.
private delegate bool EnumWC(IntPtr hwnd, IntPtr lParam);
// Import user32.dll (containing the function we need) and define
// the method corresponding to the native function.
[LibraryImport("user32.dll")]
private static partial int EnumWindows(EnumWC lpEnumFunc, IntPtr lParam);
// Define the implementation of the delegate; here, we simply output the window handle.
private static bool OutputWindow(IntPtr hwnd, IntPtr lParam)
{
Console.WriteLine(hwnd.ToInt64());
return true;
}
public static void Main(string[] args)
{
// Invoke the method; note the delegate as a first parameter.
EnumWindows(OutputWindow, IntPtr.Zero);
}
}
}
예제를 살펴보기 전에 작업해야 하는 관리되지 않는 함수의 시그니처를 검토하는 것이 좋습니다. 모든 창을 열거하기 위해 호출할 함수에는 다음과 같은 시그니처가 있습니다. BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);
첫 번째 매개 변수는 콜백입니다. 이 콜백에는 다음과 같은 시그니처가 있습니다. BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);
이제 예를 살펴보겠습니다.
- 예제의 줄 #9에서는 비관리 코드의 콜백 시그니처와 일치하는 대리자를 정의합니다. 관리 코드에서
IntPtr
을 사용하여 LPARAM 및 HWND 형식을 나타내는 방법을 확인합니다. - 줄 #13 및 #14에서는 user32.dll 라이브러리의
EnumWindows
함수를 도입합니다. - 줄 #17-20에서는 대리자를 구현합니다. 이 간단한 예제에서는 단순히 핸들을 콘솔에 출력하려고 합니다.
- 마지막으로, 줄 #24에서는 외부 메서드를 호출하고 대리자에 전달합니다.
Linux 및 macOS 예제는 다음과 같습니다. 해당 예제에서는 C 라이브러리 libc
에 있는 ftw
함수를 사용합니다. 이 함수는 디렉터리 계층 구조를 트래버스하는 데 사용되며, 함수에 대한 포인터를 해당 매개 변수 중 하나로 사용합니다. 이 함수에는 다음과 같은 시그니처가 있습니다. int (*fn) (const char *fpath, const struct stat *sb, int typeflag)
.
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Define a delegate that has the same signature as the native function.
private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);
// Import the libc and define the method to represent the native function.
[LibraryImport("libc.so.6", StringMarshalling = StringMarshalling.Utf16)]
private static partial int ftw(string dirpath, DirClbk cl, int descriptors);
// Implement the above DirClbk delegate;
// this one just prints out the filename that is passed to it.
private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
{
Console.WriteLine(fName);
return 0;
}
public static void Main(string[] args)
{
// Call the native function.
// Note the second parameter which represents the delegate (callback).
ftw(".", DisplayEntry, 10);
}
}
// The native callback takes a pointer to a struct. This type
// represents that struct in managed code.
[StructLayout(LayoutKind.Sequential)]
public struct Stat
{
public uint DeviceID;
public uint InodeNumber;
public uint Mode;
public uint HardLinks;
public uint UserID;
public uint GroupID;
public uint SpecialDeviceID;
public ulong Size;
public ulong BlockSize;
public uint Blocks;
public long TimeLastAccess;
public long TimeLastModification;
public long TimeLastStatusChange;
}
}
macOS 예제에서는 동일한 함수를 사용하며, macOS에서 libc
가 다른 위치에 유지되므로 LibraryImport
특성에 대한 인수만 다릅니다.
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Define a delegate that has the same signature as the native function.
private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);
// Import the libc and define the method to represent the native function.
[LibraryImport("libSystem.dylib", StringMarshalling = StringMarshalling.Utf16)]
private static partial int ftw(string dirpath, DirClbk cl, int descriptors);
// Implement the above DirClbk delegate;
// this one just prints out the filename that is passed to it.
private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
{
Console.WriteLine(fName);
return 0;
}
public static void Main(string[] args)
{
// Call the native function.
// Note the second parameter which represents the delegate (callback).
ftw(".", DisplayEntry, 10);
}
}
// The native callback takes a pointer to a struct. This type
// represents that struct in managed code.
[StructLayout(LayoutKind.Sequential)]
public struct Stat
{
public uint DeviceID;
public uint InodeNumber;
public uint Mode;
public uint HardLinks;
public uint UserID;
public uint GroupID;
public uint SpecialDeviceID;
public ulong Size;
public ulong BlockSize;
public uint Blocks;
public long TimeLastAccess;
public long TimeLastModification;
public long TimeLastStatusChange;
}
}
앞의 두 예제에서는 매개 변수를 사용하며, 두 경우 모두 매개 변수가 관리형 형식으로 지정됩니다. 런타임에서 “올바른 작업”을 수행하고 다른 쪽의 해당 항목으로 처리합니다. 형식 마샬링 페이지에서 형식이 네이티브 코드로 마샬링되는 방식을 알아봅니다.
추가 리소스
- 플랫폼 간 P/호출 작성
- 원본 생성 P/Invoke 마샬링
- C#/Win32 P/Invoke 원본 생성기는 Windows API에 대한 정의를 자동으로 생성합니다.
- C++/CLI의 P/Invoke
- P/Invoke에 대한 Mono 설명서
.NET