관리 코드에서 네이티브 함수 호출
공용 언어 런타임은 관리 코드가 네이티브 DLL(동적 연결 라이브러리)에서 C 스타일 함수를 호출할 수 있도록 하는 플랫폼 호출 서비스 또는 PInvoke를 제공합니다. 동일한 데이터 마샬링이 런타임과의 COM 상호 운용성 및 "It Just Works" 또는 IJW 메커니즘에 사용됩니다.
자세한 내용은 다음을 참조하세요.
이 섹션의 샘플에서는 사용할 수 있는 방법을 PInvoke 보여 줍니다. PInvoke 는 절차적 마샬링 코드를 작성하는 대신 특성에 선언적으로 마샬링 정보를 제공하기 때문에 사용자 지정된 데이터 마샬링을 간소화할 수 있습니다.
참고
마샬링 라이브러리는 최적화된 방식으로 네이티브 환경과 관리되는 환경 간에 데이터를 마샬링하는 다른 방법을 제공합니다. 마샬링 라이브러리에 대한 자세한 내용은 C++의 마샬링 개요 를 참조하세요. 마샬링 라이브러리는 함수가 아닌 데이터에만 사용할 수 있습니다.
PInvoke 및 DllImport 특성
다음 예제에서는 Visual C++ 프로그램의 사용을 PInvoke 보여 있습니다. 네이티브 함수 배치는 msvcrt.dll 정의됩니다. DllImport 특성은 put 선언에 사용됩니다.
// platform_invocation_services.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;
[DllImport("msvcrt", CharSet=CharSet::Ansi)]
extern "C" int puts(String ^);
int main() {
String ^ pStr = "Hello World!";
puts(pStr);
}
다음 샘플은 이전 샘플과 동일하지만 IJW를 사용합니다.
// platform_invocation_services_2.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;
#include <stdio.h>
int main() {
String ^ pStr = "Hello World!";
char* pChars = (char*)Marshal::StringToHGlobalAnsi(pStr).ToPointer();
puts(pChars);
Marshal::FreeHGlobal((IntPtr)pChars);
}
IJW의 장점
프로그램에서 사용하는 관리되지 않는 API에 대한 특성 선언을 작성
DLLImport할 필요가 없습니다. 헤더 파일을 포함하고 가져오기 라이브러리와 연결하기만 하면됩니다.IJW 메커니즘은 약간 더 빠릅니다(예를 들어 IJW 스텁은 개발자가 명시적으로 수행하므로 데이터 항목을 고정하거나 복사할 필요가 없음).
성능 문제를 명확하게 보여 줍니다. 이 경우 유니코드 문자열에서 ANSI 문자열로 변환하고 전화 교환 메모리 할당 및 할당 취소가 있다는 사실입니다. 이 경우 IJW를 사용하여 코드를 작성하는 개발자는 호출
_putws및 사용PtrToStringChars이 성능에 더 낫다는 것을 알게 됩니다.동일한 데이터를 사용하여 관리되지 않는 많은 API를 호출하는 경우 한 번 마샬링하고 마샬링된 복사본을 전달하는 것이 매번 다시 마샬링하는 것보다 훨씬 효율적입니다.
IJW의 단점
마샬링을 특성 대신 코드에서 명시적으로 지정해야 합니다(종종 적절한 기본값이 있음).
마샬링 코드는 인라인이며 애플리케이션 논리의 흐름에서 더 침습적입니다.
명시적 마샬링 API는 32비트에서 64비트 이식성을 위한 형식을 반환
IntPtr하므로 추가ToPointer호출을 사용해야 합니다.
C++에서 노출하는 특정 메서드는 더 효율적이고 명시적인 메서드이며, 몇 가지 추가적인 복잡성을 희생합니다.
애플리케이션에서 주로 관리되지 않는 데이터 형식을 사용하거나 .NET Framework API보다 관리되지 않는 API를 더 많이 호출하는 경우 IJW 기능을 사용하는 것이 좋습니다. 주로 관리되는 애플리케이션에서 비관리형 API를 호출하기 위해 선택하는 것이 더 미묘합니다.
Windows API를 사용하여 PInvoke
PInvoke는 Windows 함수를 호출하는 데 편리합니다.
이 예제에서 Visual C++ 프로그램은 Win32 API의 일부인 MessageBox 함수와 상호 운용됩니다.
// platform_invocation_services_4.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HWND;
[DllImport("user32", CharSet=CharSet::Ansi)]
extern "C" int MessageBox(HWND hWnd, String ^ pText, String ^ pCaption, unsigned int uType);
int main() {
String ^ pText = "Hello World! ";
String ^ pCaption = "PInvoke Test";
MessageBox(0, pText, pCaption, 0);
}
출력은 제목 PInvoke 테스트가 있고 텍스트 헬로 월드!를 포함하는 메시지 상자입니다.
마샬링 정보는 PInvoke에서 DLL에서 함수를 조회하는 데도 사용됩니다. user32.dll 실제로 MessageBox 함수는 없지만 CharSet=CharSet::Ansi를 사용하면 PInvoke에서 유니코드 버전인 MessageBoxW 대신 ANSI 버전인 MessageBoxA를 사용할 수 있습니다. 일반적으로 관리되지 않는 API의 유니코드 버전을 사용하는 것이 좋습니다. 이는 .NET Framework 문자열 개체의 네이티브 유니코드 형식에서 ANSI로의 변환 오버헤드를 제거하기 때문입니다.
PInvoke를 사용하지 않는 경우
PInvoke를 사용하는 것은 DLL의 모든 C 스타일 함수에 적합하지 않습니다. 예를 들어 다음과 같이 선언된 mylib.dll MakeSpecial 함수가 있다고 가정합니다.
char * MakeSpecial(char * pszString);
Visual C++ 애플리케이션에서 PInvoke를 사용하는 경우 다음과 유사한 항목을 작성할 수 있습니다.
[DllImport("mylib")]
extern "C" String * MakeSpecial([MarshalAs(UnmanagedType::LPStr)] String ^);
여기서 어려움은 MakeSpecial에서 반환된 관리되지 않는 문자열에 대한 메모리를 삭제할 수 없다는 것입니다. PInvoke를 통해 호출된 다른 함수는 사용자가 할당을 취소할 필요가 없는 내부 버퍼에 대한 포인터를 반환합니다. 이 경우 IJW 기능을 사용하는 것이 좋습니다.
PInvoke의 제한 사항
매개 변수로 사용한 네이티브 함수에서 동일한 정확한 포인터를 반환할 수 없습니다. 네이티브 함수가 PInvoke에 의해 마샬링된 포인터를 반환하는 경우 메모리 손상 및 예외가 발생할 수 있습니다.
__declspec(dllexport)
char* fstringA(char* param) {
return param;
}
다음 샘플에서는 이 문제를 나타내며 프로그램이 올바른 출력을 제공하는 것처럼 보일 수 있지만 출력은 해제된 메모리에서 가져옵니다.
// platform_invocation_services_5.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
#include <limits.h>
ref struct MyPInvokeWrap {
public:
[ DllImport("user32.dll", EntryPoint = "CharLower", CharSet = CharSet::Ansi) ]
static String^ CharLower([In, Out] String ^);
};
int main() {
String ^ strout = "AabCc";
Console::WriteLine(strout);
strout = MyPInvokeWrap::CharLower(strout);
Console::WriteLine(strout);
}
마샬링 인수
이 PInvoke경우 동일한 형식의 관리형 및 C++ 네이티브 기본 형식 간에 마샬링이 필요하지 않습니다. 예를 들어 Int32와 int 사이 또는 Double과 double 간에 마샬링이 필요하지 않습니다.
그러나 형식이 같지 않은 형식을 마샬링해야 합니다. 여기에는 char, string 및 구조체 형식이 포함됩니다. 다음 표에서는 마샬러가 다양한 형식에 사용하는 매핑을 보여 줍니다.
| wtypes.h | Visual C++ | /clr이 있는 Visual C++ | 공용 언어 런타임 |
|---|---|---|---|
| HANDLE | void * | void * | IntPtr, UIntPtr |
| BYTE | unsigned char | unsigned char | Byte |
| SHORT | short | short | Int16 |
| WORD | unsigned short | unsigned short | UInt16 |
| INT | int | int | Int32 |
| UINT | 부호 없는 정수 | 부호 없는 정수 | UInt32 |
| LONG | long | long | Int32 |
| BOOL | long | bool | 부울 |
| DWORD | unsigned long | unsigned long | UInt32 |
| ULONG | unsigned long | unsigned long | UInt32 |
| CHAR | char | char | Char |
| LPSTR | char * | String ^ [in], StringBuilder ^ [in, out] | String ^ [in], StringBuilder ^ [in, out] |
| LPCSTR | const char * | String ^ | String |
| LPWSTR | wchar_t * | String ^ [in], StringBuilder ^ [in, out] | String ^ [in], StringBuilder ^ [in, out] |
| LPCWSTR | const wchar_t * | String ^ | String |
| FLOAT | float | float | Single |
| DOUBLE | double | double | Double |
마샬러는 주소가 관리되지 않는 함수에 전달되는 경우 런타임 힙에 할당된 메모리를 자동으로 고정합니다. 고정하면 가비지 수집기가 압축 중에 할당된 메모리 블록을 이동할 수 없습니다.
이 항목의 앞부분에서 보여 준 예제에서 DllImport의 CharSet 매개 변수는 관리되는 문자열을 마샬링하는 방법을 지정합니다. 이 경우 네이티브 쪽의 ANSI 문자열로 마샬링되어야 합니다.
MarshalAs 특성을 사용하여 네이티브 함수의 개별 인수에 대한 마샬링 정보를 지정할 수 있습니다. 문자열 * 인수를 마샬링하기 위한 몇 가지 선택 항목은 BStr, ANSIBStr, TBStr, LPStr, LPWStr 및 LPTStr입니다. 기본값은 LPStr입니다.
이 예제에서 문자열은 더블 바이트 유니코드 문자열 LPWStr로 마샬링됩니다. 출력은 헬로 월드! 마샬링된 문자열의 두 번째 바이트가 null이고 put이 이를 문자열 끝 표식으로 해석하기 때문입니다.
// platform_invocation_services_3.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;
[DllImport("msvcrt", EntryPoint="puts")]
extern "C" int puts([MarshalAs(UnmanagedType::LPWStr)] String ^);
int main() {
String ^ pStr = "Hello World!";
puts(pStr);
}
MarshalAs 특성은 System::Runtime::InteropServices 네임스페이스에 있습니다. 이 특성은 배열과 같은 다른 데이터 형식과 함께 사용할 수 있습니다.
이 항목의 앞부분에서 설명한 대로 마샬링 라이브러리는 네이티브 환경과 관리되는 환경 간에 데이터를 마샬링하는 최적화된 새로운 방법을 제공합니다. 자세한 내용은 C++의 마샬링 개요를 참조하세요.
성능 고려 사항
PInvoke에는 호출당 10~30개의 x86 명령 오버헤드가 있습니다. 이 고정 비용 외에도 마샬링하면 추가 오버헤드가 발생합니다. 관리 코드와 관리되지 않는 코드에서 동일한 표현을 갖는 Blittable 형식 간에 마샬링 비용이 없습니다. 예를 들어 int와 Int32 간에 변환하는 데 드는 비용은 없습니다.
성능 향상을 위해 호출당 더 적은 데이터를 마샬링하는 더 많은 호출 대신 가능한 한 많은 데이터를 마샬링하는 PInvoke 호출을 줄입니다.