관리 코드에서 네이티브 함수 호출

공용 언어 런타임은 관리 코드가 네이티브 DLL(동적 연결 라이브러리)에서 C 스타일 함수를 호출할 수 있도록 하는 플랫폼 호출 서비스 또는 PInvoke를 제공합니다. 동일한 데이터 마샬링이 런타임과의 COM 상호 운용성 및 "It Just Works" 또는 IJW 메커니즘에 사용됩니다.

자세한 내용은 다음을 참조하세요.

이 섹션의 샘플에서는 사용 방법을 PInvoke 보여 줍니다. PInvoke 는 절차적 마샬링 코드를 작성하는 대신 특성에 선언적으로 마샬링 정보를 제공하기 때문에 사용자 지정된 데이터 마샬링을 간소화할 수 있습니다.

참고 항목

마샬링 라이브러리는 최적화된 방식으로 네이티브 환경과 관리되는 환경 간에 데이터를 마샬링하는 다른 방법을 제공합니다. 마샬링 라이브러리에 대한 자세한 내용은 C++의 마샬링 개요를 참조하세요. 마샬링 라이브러리는 함수가 아닌 데이터에만 사용할 수 있습니다.

PInvoke 및 DllImport 특성

다음 예제에서는 Visual C++ 프로그램에서 사용하는 PInvoke 방법을 보여 있습니다. 네이티브 함수 put은 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 Test가 있고 텍스트 헬로 월드!을 포함하는 메시지 상자입니다.

마샬링 정보는 PInvoke에서 DLL의 함수를 조회하는 데도 사용됩니다. user32.dll에는 실제로 MessageBox 함수가 없지만 CharSet=CharSet::Ansi를 사용하면 PInvoke에서 유니코드 버전인 MessageBoxW 대신 ANSI 버전인 MessageBoxA를 사용할 수 있습니다. 일반적으로 .NET Framework 문자열 개체의 네이티브 유니코드 형식에서 ANSI로 변환 오버헤드를 제거하므로 관리되지 않는 API의 유니코드 버전을 사용하는 것이 좋습니다.

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 부울 Boolean
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^ 문자열
LPWSTR Wchar_t* String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t* String^ 문자열
FLOAT float float 단일
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 호출 수가 줄어듭니다.

참고 항목

네이티브 및 .NET 상호 운용성