이 자습서에서는 마샬러를 구현하고 소스 생성 P/Invokes에서 사용자 지정 마샬링에 사용하는 방법을 알아봅니다.
기본 제공 형식에 대해 마샬러를 구현하고, 특정 매개 변수 및 사용자 정의 형식에 대한 마샬링을 사용자 지정하고, 사용자 정의 형식에 대한 기본 마샬링을 지정합니다.
이 자습서에 사용된 모든 소스 코드는 dotnet/samples 리포지토리에서 사용할 수 있습니다.
원본 생성기 LibraryImport
개요
이 형식은 System.Runtime.InteropServices.LibraryImportAttribute
.NET 7에 도입된 원본 생성기의 사용자 진입점입니다. 이 소스 생성기는 런타임이 아닌 컴파일 시간에 모든 마샬링 코드를 생성하도록 설계되었습니다. 진입점은 역사적으로 DllImport
를 사용하여 지정되었습니다. 그러나 이러한 접근 방식은 항상 허용 가능한 것은 아니며, 비용이 따를 수 있습니다. 자세한 내용은 P/Invoke 소스 생성을 참조하세요.
LibraryImport
원본 생성기는 모든 마샬링 코드를 생성하고 런타임 생성 요구 사항을 제거할 수 있습니다DllImport
.
런타임 및 사용자가 자신의 형식에 맞게 사용자 지정하기 위해 생성된 마샬링 코드에 필요한 세부 정보를 표현하려면 몇 가지 형식이 필요합니다. 이 자습서 전체에서 사용되는 형식은 다음과 같습니다.
MarshalUsingAttribute
– 사용 사이트에서 원본 생성기에서 찾고 특성 변수를 마샬링하기 위한 마샬러 형식을 결정하는 데 사용되는 특성입니다.CustomMarshallerAttribute
– 형식에 대한 마샬러와 마샬링 작업을 수행할 모드를 나타내는 데 사용되는 특성입니다(예: 관리에서 비관리형으로 참조).NativeMarshallingAttribute
– 특성 형식에 사용할 마샬러를 나타내는 데 사용되는 특성입니다. 이는 해당 형식에 대한 형식 및 함께 제공되는 마샬러를 제공하는 라이브러리 작성자에게 유용합니다.
그러나 이러한 특성은 사용자 지정 마샬러 작성자가 사용할 수 있는 유일한 메커니즘은 아닙니다. 원본 생성기는 마샬러 자체를 검사하여 마샬링이 어떻게 발생해야 하는지 알려주는 다양한 표시를 찾습니다.
디자인에 대한 자세한 내용은 dotnet/런타임 리포지토리에서 찾을 수 있습니다.
소스 코드 생성기 분석기 및 수정기
원본 생성기 자체와 함께 분석기와 픽서가 모두 제공됩니다. 분석기 및 해결기는 .NET 7 RC1 이후 기본적으로 사용하도록 설정되고 사용할 수 있습니다. 분석기는 개발자가 원본 생성기를 제대로 사용하도록 안내하도록 설계되었습니다. 수정 도구는 많은 DllImport
패턴들을 적절한 LibraryImport
서명으로 자동 변환해 줍니다.
네이티브 라이브러리 소개
원본 생성기를 LibraryImport
사용하는 것은 네이티브 또는 관리되지 않는 라이브러리를 사용하는 것을 의미합니다. 네이티브 라이브러리는 .NET을 통해 노출되지 않는 운영 체제 API를 직접 호출하는 공유 라이브러리(즉, .dll
.so
또는dylib
)일 수 있습니다. 라이브러리는 .NET 개발자가 사용하려는 관리되지 않는 언어로 크게 최적화된 라이브러리일 수도 있습니다. 이 자습서에서는 C 스타일 API 화면을 노출하는 고유한 공유 라이브러리를 빌드합니다. 다음 코드는 C#에서 사용할 사용자 정의 형식과 두 개의 API를 나타냅니다. 이러한 두 API는 "in" 모드를 나타내지만 샘플에서 탐색할 추가 모드가 있습니다.
struct error_data
{
int code;
bool is_fatal_error;
char32_t* message; /* UTF-32 encoded string */
};
extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintErrorData(error_data data);
앞의 코드에는 두 가지 유형의 관심 char32_t*
및 error_data
.이 포함됩니다.
char32_t*
는 UTF-32로 인코딩된 문자열을 나타내며, 이는 .NET이 역사적으로 마샬링하는 문자열 인코딩이 아닙니다.
error_data
는 32비트 정수 필드, C++ 부울 필드 및 UTF-32로 인코딩된 문자열 필드를 포함하는 사용자 정의 형식입니다. 이러한 두 형식 모두 소스 생성기가 마샬링 코드를 생성하는 방법을 제공해야 합니다.
기본 제공 형식에 대한 마샬링 사용자 지정
먼저 char32_t*
형식을 고려합니다. 사용자 정의 형식 때문에 이 형식을 마샬링해야 합니다.
char32_t*
는 네이티브 쪽을 나타내지만 관리 코드의 표현도 필요합니다. .NET에는 "string" 형식 string
이 하나뿐입니다. 따라서 네이티브 UTF-32로 인코딩된 문자열을 관리 코드의 string
형식과 상호 변환할 것입니다. UTF-8, UTF-16, ANSI 및 Windows string
형식으로 마샬링하는 BSTR
형식을 위해 몇 가지 기본 제공 마샬러가 이미 있습니다. 그러나 UTF-32로 마샬링하는 방법이 없습니다. 이것이 정의해야 하는 사항입니다.
Utf32StringMarshaller
이 형식은 원본 생성기에 대해 수행하는 작업을 설명하는 특성으로 표시됩니다CustomMarshaller
. 특성에 대한 첫 번째 형식 인수는 string
형식, 마샬링할 관리되는 형식, 두 번째는 마샬러를 사용할 시기를 나타내는 모드이고, 세 번째 형식은 마샬링에 사용할 형식입니다 Utf32StringMarshaller
. 여러 번 적용 CustomMarshaller
하여 모드 및 해당 모드에 사용할 마샬러 형식을 추가로 지정할 수 있습니다.
현재 예제에서는 일부 입력을 사용하고 마샬링된 형식으로 데이터를 반환하는 "무상태" 마샬링 도구를 보여 줍니다. 이 메서드는 비관리 마샬링과의 대칭을 위해 존재하며, 가비지 수집기는 관리 마샬러에 대한 "'무료' 작업" 역할을 합니다. 구현자는 입력을 출력으로 마샬링하는 데 필요한 모든 작업을 자유롭게 수행할 수 있지만 원본 생성기에서 명시적으로 유지되는 상태는 없습니다.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
internal static unsafe class Utf32StringMarshaller
{
public static uint* ConvertToUnmanaged(string? managed)
=> throw new NotImplementedException();
public static string? ConvertToManaged(uint* unmanaged)
=> throw new NotImplementedException();
public static void Free(uint* unmanaged)
=> throw new NotImplementedException();
}
}
이 특정 마샬러가 string
에서 char32_t*
로 변환을 수행하는 방법에 대한 자세한 내용은 샘플에서 확인할 수 있습니다. 모든 .NET API를 사용할 수 있습니다(예: Encoding.UTF32).
상태가 바람직한 경우를 상정해 봅시다. 추가 CustomMarshaller
모드를 관찰하고 보다 구체적인 모드 MarshalMode.ManagedToUnmanagedIn
를 확인합니다. 이 전문 마샬러는 "상태 저장" 방식으로 구현되며, 상호 운용 호출 동안 상태를 저장할 수 있습니다. 더 많은 전문화와 상태 허용 최적화, 그리고 모드에 맞춘 마샬링. 예를 들어 마샬링하는 동안 명시적 할당을 방지할 수 있는 스택 할당 버퍼를 제공하도록 원본 생성기를 지시할 수 있습니다. 스택 할당 버퍼에 대한 지원을 나타내기 위해 마샬러는 BufferSize
속성과 FromManaged
형식의 Span
를 인수로 받는 unmanaged
메서드를 구현합니다. 이 속성은 BufferSize
마샬러가 마샬링 호출 중에 가져올 스택 공간, 즉 Span
에 전달될 FromManaged
의 길이를 나타냅니다.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
[CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToUnmanagedIn))]
internal static unsafe class Utf32StringMarshaller
{
//
// Stateless functions removed
//
public ref struct ManagedToUnmanagedIn
{
public static int BufferSize => 0x100;
private uint* _unmanagedValue;
private bool _allocated; // Used stack alloc or allocated other memory
public void FromManaged(string? managed, Span<byte> buffer)
=> throw new NotImplementedException();
public uint* ToUnmanaged()
=> throw new NotImplementedException();
public void Free()
=> throw new NotImplementedException();
}
}
}
이제 UTF-32 문자열 마샬러를 사용하여 두 네이티브 함수 중 첫 번째 함수를 호출할 수 있습니다. 다음 선언에서는 LibraryImport
특성을 사용하며 DllImport
특성과 마찬가지로 작동하지만, 네이티브 함수를 호출할 때 사용할 마샬러를 소스 생성기에 알리기 위해 MarshalUsing
특성에 의존합니다. 상태 없는 마샬러나 상태 있는 마샬러를 사용해야 하는지 명확히 할 필요가 없습니다. 이는 구현자가 마샬러의 MarshalMode
속성에 CustomMarshaller
을 정의하여 처리합니다. 소스 생성기는 MarshalUsing
가 적용되는 컨텍스트에 따라 가장 적합한 마샬러를 선택하며, MarshalMode.Default
는 대체 옵션으로 사용됩니다.
// extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);
사용자 정의 형식에 대한 마샬링 사용자 지정
사용자 정의 형식을 마샬링하려면 마샬링 논리뿐만 아니라, 마샬링할 대상이 되는 C#의 형식도 정의해야 합니다. 마샬링하려는 네이티브 유형을 기억하세요.
struct error_data
{
int code;
bool is_fatal_error;
char32_t* message; /* UTF-32 encoded string */
};
이제 C#에서 이상적으로 어떻게 표시되는지 정의합니다. 최신 int
C++와 .NET 모두에서 크기가 동일합니다. A bool
는 .NET의 부울 값에 대한 정식 예제입니다.
Utf32StringMarshaller
을(를) 기반으로 구축하면, char32_t*
을(를) .NET string
으로 마샬링할 수 있습니다. .NET 스타일을 고려하면 결과는 C#의 다음 정의입니다.
struct ErrorData
{
public int Code;
public bool IsFatalError;
public string? Message;
}
명명 패턴에 따라 마샬러의 이름을 지정합니다 ErrorDataMarshaller
. 마샬러 MarshalMode.Default
를 지정하는 대신 일부 모드에 대해서만 마샬러를 정의합니다. 이 경우 마샬러가 제공되지 않는 모드에 사용되는 경우 원본 생성기가 실패합니다. "in" 방향에 대한 마샬러 정의부터 시작합니다. "이것은 static
함수로만 구성되어 있기 때문에 '상태 비저장' 마샬러입니다."
namespace CustomMarshalling
{
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
internal static unsafe class ErrorDataMarshaller
{
// Unmanaged representation of ErrorData.
// Should mimic the unmanaged error_data type at a binary level.
internal struct ErrorDataUnmanaged
{
public int Code; // .NET doesn't support less than 32-bit, so int is 32-bit.
public byte IsFatal; // The C++ bool is defined as a single byte.
public uint* Message; // This could be as simple as a void*, but uint* is closer.
}
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
=> throw new NotImplementedException();
public static void Free(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
}
}
ErrorDataUnmanaged
는 관리되지 않는 형식의 모양을 모방합니다.
ErrorData
에서 ErrorDataUnmanaged
로의 변환은 이제 Utf32StringMarshaller
를 사용하여 간단합니다.
관리되지 않는 코드와 관리 코드에서 표현이 동일하기 때문에 int
의 마샬링 작업이 필요하지 않습니다.
bool
값의 이진 표현은 .NET에서 정의되지 않으므로 현재 값을 사용하여 관리되지 않는 형식에서 0과 0이 아닌 값을 정의합니다. 그런 다음, UTF-32 마샬러를 다시 사용하여 string
필드를 uint*
으로 변환합니다.
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
return new ErrorDataUnmanaged
{
Code = managed.Code,
IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
};
}
이 마샬러를 "in"으로 정의하므로 마샬링 중에 수행되는 모든 할당을 정리해야 합니다.
int
필드와 bool
필드는 메모리를 할당하지 않았지만 Message
필드는 할당했습니다.
Utf32StringMarshaller
를 다시 사용하여 마샬링된 문자열을 정리합니다.
public static void Free(ErrorDataUnmanaged unmanaged)
=> Utf32StringMarshaller.Free(unmanaged.Message);
"나가는" 시나리오를 간단히 살펴보겠습니다. 하나 이상의 인스턴스가 반환되는 경우를 고려합니다 error_data
.
extern "C" DLL_EXPORT error_data STDMETHODCALLTYPE GetFatalErrorIfNegative(int code)
extern "C" DLL_EXPORT error_data* STDMETHODCALLTYPE GetErrors(int* codes, int len)
[LibraryImport(LibName)]
internal static partial ErrorData GetFatalErrorIfNegative(int code);
[LibraryImport(LibName)]
[return: MarshalUsing(CountElementName = "len")]
internal static partial ErrorData[] GetErrors(int[] codes, int len);
컬렉션이 아닌 단일 인스턴스 형식을 반환하는 P/Invoke는 로 분류됩니다 MarshalMode.ManagedToUnmanagedOut
. 일반적으로 컬렉션을 사용하여 여러 요소를 반환하며, 이 경우 Array
컬렉션이 사용됩니다. 모드에 해당하는 컬렉션 시나리오의 MarshalMode.ElementOut
마샬러는 여러 요소를 반환하며 나중에 설명합니다.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
internal static unsafe class ErrorDataMarshaller
{
//
// Other marshallers removed
//
public static class Out
{
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
=> throw new NotImplementedException();
public static void Free(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
}
}
}
ErrorDataUnmanaged
에서 ErrorData
로의 전환은 "in" 모드에서 수행한 조작의 반대입니다. 또한 관리되지 않는 환경에서 수행해야 하는 모든 할당을 정리해야 합니다. 표시된 static
함수가 "상태 비저장"이며, 상태 비저장임이 모든 "요소" 모드에 대한 요구 사항임을 명심하는 것도 중요합니다. "in" 모드와 같은 메서드 ConvertToUnmanaged
가 있음을 또한 알 수 있습니다. 모든 "요소" 모드는 "in" 및 "out" 모드 모두에 대한 처리가 필요합니다.
관리되지 않는 "아웃" 마샬러를 위해, 특별한 작업을 수행해야 합니다. 마샬링하는 데이터 형식의 이름은 error_data
이며, .NET은 일반적으로 오류를 예외로 표현합니다. 일부 오류는 다른 오류보다 더 큰 영향을 미치며 "치명적"으로 식별되는 오류는 일반적으로 치명적이거나 복구할 수 없는 오류를 나타냅니다.
error_data
오류가 심각한지 확인할 필드가 있습니다. 관리 코드로 error_data
를 전달하고, 치명적인 경우에는 ErrorData
로 변환하고 반환하는 대신 예외를 발생시킵니다.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedOut, typeof(ThrowOnFatalErrorOut))]
internal static unsafe class ErrorDataMarshaller
{
//
// Other marshallers removed
//
public static class ThrowOnFatalErrorOut
{
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
public static void Free(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
}
}
}
"out" 매개 변수는 관리되지 않는 컨텍스트에서 관리되는 컨텍스트로 변환되므로 메서드를 구현합니다 ConvertToManaged
. 관리되지 않는 피호출자가 객체를 ErrorDataUnmanaged
반환하면, ElementOut
모드 마샬러를 사용하여 해당 객체를 검사하고 치명적인 오류로 표시되었는지 확인할 수 있습니다. 그렇다면, 그것은 단지 ErrorData
을 반환하는 대신 throw 하라는 신호입니다.
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
ErrorData data = Out.ConvertToManaged(unmanaged);
if (data.IsFatalError)
throw new ExternalException(data.Message, data.Code);
return data;
}
네이티브 라이브러리를 사용할 뿐만 아니라 커뮤니티와 작업을 공유하고 interop 라이브러리를 제공하려고 할 수도 있습니다.
ErrorData
P/Invoke에서 사용할 때마다 암시적 마샬러를 제공하려면 [NativeMarshalling(typeof(ErrorDataMarshaller))]
정의에 ErrorData
을(를) 추가할 수 있습니다. 이제 LibraryImport
호출에서 이 유형의 정의를 사용하는 사람들은 마샬러의 이점을 받을 수 있습니다. 항상 사용자가 사용 사이트에서 MarshalUsing
를 활용하여 마샬러를 재정의할 수 있습니다.
[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }
참고하십시오
.NET