플랫폼 호출 서비스

플랫폼 호출 서비스, 일반적으로 P/Invoke라고 불리는 이 기능은 마이크로소프트의 공통 언어 런타임과 같은 공통 언어 기반 구현의 한 기능으로, 관리 코드가 네이티브 코드를 호출할 수 있도록 한다.

C# 또는 VB.NET과 같은 관리 코드는 닷넷 프레임워크를 구성하는 라이브러리에 정의된 클래스, 메서드 및 형식에 대한 네이티브 액세스를 제공한다. 닷넷 프레임워크는 광범위한 기능을 제공하지만, 일반적으로 비관리 코드로 작성된 많은 하위 수준 운영 체제 라이브러리나 타사 라이브러리에 대한 액세스가 부족할 수 있다. P/Invoke는 프로그래머가 이러한 라이브러리의 함수에 액세스하는 데 사용할 수 있는 기술이다. 이러한 라이브러리 내의 함수 호출은 관리 코드 내에서 비관리 함수의 서명을 선언함으로써 이루어지며, 이는 다른 관리 메서드처럼 호출될 수 있는 실제 함수 역할을 한다. 이 선언은 라이브러리의 파일 경로를 참조하고, 공통 언어 런타임(CLR)에 의해 비관리 형식으로 그리고 비관리 형식에서 암시적으로 마샬링될 가능성이 가장 높은 관리 형식으로 함수 매개변수와 반환 값을 정의한다. 비관리 데이터 형식이 관리 형식으로의 단순한 암시적 변환에 비해 너무 복잡해지는 경우, 프레임워크는 사용자가 함수, 반환 값 및 매개변수에 속성을 정의하여 데이터가 어떻게 마샬링되어야 하는지를 명시적으로 세분화할 수 있도록 하여 암시적으로 수행하려 할 때 예외가 발생하지 않도록 한다.

비관리 언어로 프로그래밍하는 것에 비해 관리 코드 프로그래머에게는 많은 하위 수준 프로그래밍 개념의 추상화가 제공된다. 결과적으로, 관리 코드 경험만 있는 프로그래머는 P/Invoke 사용 시 발생할 수 있는 일부 장애물을 극복하기 위해 포인터, 구조체 및 참조에 의한 전달과 같은 프로그래밍 개념을 익혀야 할 것이다.

아키텍처

개요
현재 사용되는 P/Invoke의 두 가지 변형은 다음과 같다.

명시적
 동적 링크 라이브러리(DLL)를 통해 네이티브 코드가 가져와진다.
 호출자 어셈블리에 포함된 메타데이터는 네이티브 코드가 호출되는 방법과 데이터가 액세스되는 방법을 정의한다(일반적으로 마샬링 글루 생성을 돕기 위해 속성 지정된 소스 지정자가 필요하다).
 이 정의가 "명시적" 부분이다.

암시적
 C++/CLI를 사용함으로써, 애플리케이션은 명시적 선언 없이 관리 힙(추적 포인터를 통해)과 모든 네이티브 메모리 영역을 동시에 사용할 수 있다. (암시적)
 이 경우 주요 이점은 기본 네이티브 데이터 구조가 변경되더라도 이름이 호환되는 한 중대한 변경을 피할 수 있다는 것이다.
 즉, 네이티브 헤더에서 구조체를 추가/제거/재정렬하는 것은 구조체 멤버 이름이 변경되지 않는 한 투명하게 지원된다.

세부 사항
P/Invoke를 사용할 때, CLR은 DLL 로딩과 비관리 이전 형식들을 CTS 형식으로 변환하는 것을 처리한다(또한 매개변수 마샬링이라고도 한다). 이를 수행하기 위해 CLR은 다음을 수행한다.
 함수를 포함하는 DLL을 찾는다.
 DLL을 메모리에 로드한다.
 메모리에서 함수의 주소를 찾고, 필요에 따라 데이터를 마샬링하면서 해당 인수를 스택에 푸시한다.

P/Invoke는 표준(비관리) C 또는 C++ DLL을 사용하는 데 유용하다. 프로그래머가 광범위한 윈도우 API에 액세스해야 할 때 사용할 수 있는데, 윈도우 라이브러리에서 제공하는 많은 함수에 사용 가능한 래퍼가 부족하기 때문이다. 닷넷 프레임워크에서 Win32 API가 노출되지 않으면 이 API의 래퍼를 수동으로 작성해야 한다.

함정
P/Invoke 래퍼를 작성하는 것은 어렵고 오류가 발생하기 쉽다. 네이티브 DLL을 사용한다는 것은 프로그래머가 더 이상 .NET 환경에서 일반적으로 제공되는 형식 안전성 및 가비지 컬렉션의 이점을 누릴 수 없음을 의미한다. 이를 부적절하게 사용하면 세그멘테이션 오류 또는 메모리 누수와 같은 문제가 발생할 수 있다. .NET 환경에서 사용할 레거시 함수의 정확한 서명을 얻는 것은 어려울 수 있으며, 이는 그러한 문제를 초래할 수 있다. 이러한 목적으로 서명 문제를 방지하는 데 도움이 되는 도구와 웹사이트가 존재한다. 

다른 함정은 다음과 같다.
 관리 언어에서 사용자 정의 형식의 잘못된 데이터 구조 정렬: C 언어에서는 컴파일러나 컴파일러 지시문에 따라 데이터를 정렬하는 여러 가지 방법이 있으며, CLR에 비블리터블 타입의 데이터를 정렬하는 방법을 명시적으로 알려주도록 주의해야 한다. 이의 일반적인 예는 C의 유니온을 나타내기 위해 .NET에서 데이터 형식을 정의하려고 할 때 발생한다. 두 개의 다른 변수가 메모리에서 겹치는데, .NET에서 이 두 변수를 형식으로 정의하면 메모리의 다른 위치에 있게 되므로 문제를 해결하기 위해 특수 속성을 사용해야 한다.
 관리 언어의 가비지 수집기가 데이터 위치에 간섭하는 경우: .NET의 메서드에 참조가 로컬이고 네이티브 함수로 전달되면, 관리 메서드가 반환될 때 가비지 수집기가 해당 참조를 회수할 수 있다. 개체 참조가 고정되어 가비지 수집기에 의해 수집되거나 이동되는 것을 방지하도록 주의해야 하며, 이는 네이티브 모듈에 의한 잘못된 액세스를 초래할 수 있다.

C++/CLI를 사용할 때, 내보내진 CIL은 관리 힙에 위치한 개체 및 동시에 주소 지정 가능한 모든 네이티브 메모리 위치와 자유롭게 상호 작용할 수 있다. 관리 힙에 상주하는 개체는 단순한 "object->field;" 표기법을 사용하여 값을 할당하거나 메서드 호출을 지정하는 방식으로 호출, 수정 또는 구성될 수 있다. 불필요한 컨텍스트 전환을 제거하고 메모리 요구 사항을 줄임으로써(더 짧은 스택) 상당한 성능 향상을 얻을 수 있다.

하지만 새로운 문제들도 발생한다.
 특별히 처리하지 않으면 코드가 이중 썽킹(Double Thunking)에 취약해진다.
 로더 잠금 문제

이러한 참조들은 각 문제에 대한 해결책을 제시한다. 주요 이점은 구조 선언의 제거이며, 필드 선언 순서 및 정렬 문제는 C++ 인터롭스 환경에서는 존재하지 않는다.

예시

기본 예시
이 첫 번째 간단한 예시는 특정 DLL의 버전을 얻는 방법을 보여준다.

윈도우 API의 DllGetVersion 함수 서명:
HRESULT DllGetVersion(DLLVERSIONINFO* pdvi)

DllGetVersion 함수를 호출하는 P/Invoke C# 코드:

[StructLayout(LayoutKind.Sequential)]
private struct DLLVERSIONINFO {
 public int cbSize;
 public int dwMajorVersion;
 public int dwMinorVersion;
 public int dwBuildNumber;
 public int dwPlatformID;
}

[DllImport("shell32.dll")]
static extern int DllGetVersion(ref DLLVERSIONINFO pdvi);

두 번째 예는 파일에서 아이콘을 추출하는 방법을 보여준다.

윈도우 API의 ExtractIcon 함수 서명:
HICON ExtractIcon(HINSTANCE hInst, LPCTSTR lpszExeFileName, UINT nIconIndex);

ExtractIcon 함수를 호출하는 P/Invoke C# 코드:
[DllImport("shell32.dll")]
static extern IntPtr ExtractIcon(
 IntPtr hInst,
 [MarshalAs(UnmanagedType.LPStr)] string lpszExeFileName,
 uint nIconIndex
);

다음 복잡한 예시는 윈도우 플랫폼에서 두 프로세스 간에 이벤트를 공유하는 방법을 보여준다.

CreateEvent 함수 서명:
HANDLE CreateEvent(
 LPSECURITY_ATTRIBUTES lpEventAttributes,
 BOOL bManualReset,
 BOOL bInitialState,
 LPCTSTR lpName
);

CreateEvent 함수를 호출하는 P/Invoke C# 코드:
[DllImport("kernel32.dll", SetLastError=true)]
static extern IntPtr CreateEvent(
 IntPtr lpEventAttributes,
 bool bManualReset,
 bool bInitialState,
 [MarshalAs(UnmanagedType.LPStr)] string lpName
);

더 복잡한 예시
// native declaration
typedef struct _PAIR {
 DWORD Val1;
 DWORD Val2;
} PAIR, *PPAIR;

// Compiled with /clr; use of #pragma managed/unmanaged can lead to double thunking;
// avoid by using a stand-alone .cpp with .h includes.
// This would be located in a .h file.

// Note use of de/referencing. It must match your use.
template <>
inline CLR_PAIR^ MarshalAs
(const PAIR& Src) {
 CLR_PAIR^ Dest = gcnew CLR_PAIR;
 Dest->Val1 = Src.Val1;
 Dest->Val2 = Src.Val2;
 return Dest;
};

// ...

CLR_PAIR^ mgdPair1;
CLR_PAIR^ mgdPair2;
PAIR native0;
PAIR* native1 = &native0

native0 = NativeCallGetRefToMemory();

// Using marshal_as. It makes sense for large or frequently used types.
mgdPair1 = MarshalAs
(*native1);

// Direct field use
mgdPair2->Val1 = native0.Val1;
mgdPair2->Val2 = native0.Val2;

return(mgd_pair1); // Return to C#

도구

P/Invoke 서명 생성을 돕기 위해 설계된 여러 도구가 있다.

C++ 헤더 파일과 네이티브 DLL 파일을 가져와서 인터페이스 어셈블리를 자동으로 생성하는 유틸리티 애플리케이션을 작성하는 것은 상당히 어려운 일로 판명되었다. P/Invoke 서명에 대한 이러한 가져오기/내보내기를 생성하는 주된 문제는 일부 C++ 함수 호출 매개변수 유형의 모호성 때문이다.

브래드 에이브람스는 이 주제에 대해 다음과 같이 말했다.

문제는 다음과 같은 C++ 함수에 있다.
__declspec(dllexport) void MyFunction(char* params);

P/Invoke 서명에서 매개변수 params에 어떤 유형을 사용해야 할까? 이것은 C++ 널로 끝나는 문자열이거나, 배열이거나, 출력 매개변수일 수 있다. 따라서 , , 또는 를 사용해야 할까?

이 문제와 상관없이 P/Invoke 서명 생성을 단순화하는 몇 가지 도구가 있다.

아래 나열된 도구 중 하나인 xInterop C++ .NET Bridge는 .NET 환경에서 동일한 C++ 메서드의 여러 오버로드를 구현함으로써 이 문제를 해결했으며, 개발자는 올바른 오버로드를 선택하여 호출할 수 있다.

PInvoke.net
PInvoke.net은 많은 표준 윈도우 API의 P/Invoke 서명을 포함하는 위키이다. Redgate Software가 소유하고 있으며 월간 약 50,000건의 조회수를 기록한다.

서명은 위키 사용자들이 수동으로 작성한다. 마이크로소프트 비주얼 스튜디오의 무료 추가 기능을 사용하여 검색할 수 있다.

PInvoker
PInvoker는 네이티브 DLL 및 C++ .h 파일을 가져와서 완전히 형성되고 컴파일된 인터롭 DLL을 내보내는 애플리케이션이다. PInvoker는 네이티브 포인터 함수 매개변수를 PInvoker 특정 .NET 인터페이스 클래스로 래핑하여 모호성 문제를 극복한다. P/Invoke 메서드 정의에서 표준 .NET 매개변수 유형(, 등) 대신 이 인터페이스 클래스를 P/Invoke 함수 호출에 사용한다.

예를 들어, 위 예제 코드를 고려하면 PInvoker는 네이티브 를 래핑하는 .NET 인터페이스 클래스를 허용하는 .NET 함수를 생성할 것이다. 이 클래스의 생성은 또는 에서 이루어질 수 있다. 두 경우 모두 실제 네이티브 메모리 구조는 동일하지만, 각 유형에 대한 해당 인터페이스 클래스 생성자는 메모리를 다른 방식으로 채울 것이다. 따라서 함수에 전달해야 할 .NET 유형을 결정하는 책임은 개발자에게 넘어간다.

Microsoft Interop Assistant
Microsoft Interop Assistant는 코드플렉스에서 바이너리 및 소스 코드를 다운로드할 수 있는 무료 도구이다. Microsoft Limited Public License(Ms-LPL)에 따라 라이선스가 부여된다.

두 부분으로 구성된다.
 및 메서드 정의를 포함하는 네이티브 C++ 헤더 파일 코드의 작은 섹션을 가져오는 변환기. 그런 다음 애플리케이션에 복사하여 붙여넣을 수 있는 C# P/Invoke 코드를 생성한다.
 변환된 윈도우 API 상수, 메서드 및 구조 정의에 대한 검색 가능한 데이터베이스.

이 도구는 컴파일된 DLL 대신 C# 소스 코드를 생성하므로 사용자는 사용하기 전에 코드에 필요한 모든 변경을 자유롭게 할 수 있다. 따라서 모호성 문제는 P/Invoke 메서드 서명에 사용할 특정 .NET 유형을 애플리케이션이 선택하고 필요한 경우 사용자가 이를 필요한 유형으로 변경할 수 있도록 하여 해결된다.

P/Invoke Wizard
P/Invoke Wizard는 Microsoft Interop Assistant와 유사한 방식을 사용한다. 네이티브 C++ .h 파일 코드를 받아들여 .NET 애플리케이션 코드에 붙여넣을 수 있는 C# (또는 VB.NET) 코드를 생성한다.

또한 데스크톱용 .NET Framework 또는 윈도우 모바일 스마트 장치(및 윈도우 CE)용 .NET Compact Framework 중에서 대상으로 지정할 프레임워크를 선택할 수 있는 옵션도 제공한다.

 xInterop C++ .NET Bridge 
xInterop C++ .NET Bridge는 네이티브 C++ DLL용 C# 래퍼와 .NET 어셈블리에 액세스하는 C++ 브리지를 생성하는 윈도우 애플리케이션이다. 여기에는 표준 C++ 클래스(예: string, iostream 등)를 래핑하는 C#/.NET 라이브러리가 함께 제공되며, .NET에서 C++ 클래스 및 개체에 액세스할 수 있다.

이 도구는 기존 네이티브 C++ DLL과 C# 래퍼 DLL을 빌드하는 데 필요한 관련 헤더 파일에서 소스 코드가 포함된 C# 래퍼 DLL을 생성한다. P/Invoke 서명 및 데이터 마샬링은 애플리케이션에 의해 생성된다. 결과 C# 래퍼는 매개변수 유형이 .NET 코드로 변환된 C++ 대응물과 유사한 인터페이스를 갖는다.

이 도구는 C++ DLL에서 내보내지지 않은 템플릿 클래스를 인식하고 템플릿 클래스를 인스턴스화하여 보조 DLL로 내보내며, 해당 C++ 인터페이스는 .NET에서 사용될 수 있다.

 같이 보기 
 블리터블 타입
 자바 네이티브 인터페이스, 자바 프로그램이 네이티브 코드에 액세스하는 표준 방식
 자바 네이티브 액세스, P/Invoke의 자바 버전
 윈도우 라이브러리 파일
 J/Direct, 마이크로소프트 자바 가상 머신의 더 이상 유지 관리되지 않는 동등한 API

 각주 

 외부 링크 
 P/Invoke 전용 사이트

분류:공통 언어 기반
분류:닷넷 용어