.NET 8 - 함수 포인터에 대한 Reflection 정보 조회
문서를 보면,
Reflection improvements
; https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#reflection-improvements
.NET 5/C# 9.0에 추가되었던 함수 포인터에 대해,
C# 9.0 - (6) 함수 포인터(Function pointers)
; https://www.sysnet.pe.kr/2/0/12374
Reflection 대응이 가능해졌다고 합니다. 예제를 위해,
C++의 inline asm 사용을 .NET으로 포팅하는 방법
; https://www.sysnet.pe.kr/2/0/1267
저 글의 CPUID를 구하는 코드를 (기존의 System.Delegate를 사용하던 방식 대신) 함수 포인터를 이용해 다음과 같이 처리해 보겠습니다.
public class SystemInfo
{
// ...[생략]...
delegate* unmanaged[Cdecl]<byte*, void> _cpuIdFunc;
public SystemInfo()
{
byte[] codeBytes = (IntPtr.Size == 4) ? x86CpuIdBytes : x64CpuIdBytes;
_codePointer = VirtualAlloc(IntPtr.Zero, new UIntPtr((uint)codeBytes.Length),
AllocationType.COMMIT | AllocationType.RESERVE, MemoryProtection.EXECUTE_READWRITE
);
Marshal.Copy(codeBytes, 0, _codePointer, codeBytes.Length);
_cpuIdFunc = (delegate* unmanaged[Cdecl]<byte*, void>)_codePointer;
}
// ...[생략]...
}
.NET 7까지는 "delegate* unmanaged[Cdecl]<byte*, void>" 타입은 단순히 포인터에 불과했지만, .NET 8부터는 함수 포인터 타입으로써의 정보를 구할 수 있게 바뀐 것인데요, 그렇다고 해서 _cpuIdFunc 필드에 대고 GetType을 할 수는 없습니다.
Type type = _cpuIdFunc.GetType(); // 컴파일 오류: CS1061
// 또는,
Type type = typeof(_cpuIdFunc); // 컴파일 오류
대신 delegate 정의에 대해 직접 타입을 구하거나,
Type type = typeof(delegate* unmanaged[Cdecl, SuppressGCTransition]<byte*, void>);
클래스의 멤버 필드로 조회해 타입을 구할 수 있습니다.
FieldInfo fieldType = typeof(SystemInfo).GetField(
nameof(SystemInfo._cpuIdFunc), BindingFlags.NonPublic | BindingFlags.Instance);
Type type = fieldType.FieldType;
특이한 점은, 타입으로써의 Name도 없고 BaseType도 없다는 점입니다.
Console.WriteLine(type.IsClass); // True (타입이긴 해도!)
Console.WriteLine(type.FullName ?? "(null)"); // (null)
Console.WriteLine(type.BaseType ?? "(null)"); // (null)
Console.WriteLine(type); // System.Void(System.Byte*)
단지 해당 타입이 포인터라는 점과,
Console.WriteLine(type.IsFunctionPointer); // True
Console.WriteLine(type.IsUnmanagedFunctionPointer); // True
Console.WriteLine(type.IsPointer); // False (그렇다고 해서 포인터는 아니라는!)
"함수"를 가리키기 때문에 그에 대한 매개변수 정보, 반환값 정보를 구할 수 있도록 했습니다.
Console.WriteLine(type.GetFunctionPointerReturnType()); // System.Void (반환 타입)
foreach (Type parameterType in type.GetFunctionPointerParameterTypes()) // 매개변수 타입
{
Console.WriteLine($"Parameter type: {parameterType}"); // Parameter type: System.Byte*
}
재미있는 점은, delegate* 타입에 부여한 calling convention 정보를 가져오기 위해 Type이 아닌, FieldInfo의 (.NET 8에 새롭게 추가된)
GetModifiedFieldType 메서드를 통해서만 가능하다는 점입니다.
FieldInfo fieldInfo = typeof(SystemInfo).GetField(
nameof(SystemInfo._cpuIdFunc), BindingFlags.NonPublic | BindingFlags.Instance);
Type type = fieldInfo.FieldType;
Type modifiedType = fieldInfo.GetModifiedFieldType();
Console.WriteLine(type == modifiedType); // False
Console.WriteLine(type == modifiedType.UnderlyingSystemType); // True
foreach (Type callConv in modifiedType.GetFunctionPointerCallingConventions())
{
Console.WriteLine($"Calling convention: {callConv}"); // Calling convention: System.Runtime.CompilerServices.CallConvCdecl
}
결국, "delegate*"로부터 구한 타입의 경우에는 그 안에 Calling convention에 대한 정보가 있음에도 불구하고 그 정보를 구할 수 없다는 요상한 경우가 나옵니다.
Type type = typeof(delegate* unmanaged[Cdecl]<byte*, void>);
Console.WriteLine($"{type.GetFunctionPointerCallingConventions().Length}"); // 0
마찬가지로, 매개변수에 대한 접근자 정보 역시 GetModifiedFieldType으로 구한 타입으로만 조회할 수 있습니다.
// 제가 만든 예제 코드에서는 modifier가 없으므로 결과가 없습니다.
var modifiers =
modifiedType.GetFunctionPointerParameterTypes()[0].GetRequiredCustomModifiers();
foreach (Type modreq in modifiers) // [0] 번째 인자의 modifier를 열거
{
Console.WriteLine($"Required modifier for first parameter: {modreq}");
}
사실 개인적으로는, 함수 포인터의 Reflection 조회 자체에 대해서는 별로 관심은 없었고, 해당 문서를 보면서 보게 된 코드가 더 흥미로웠습니다.
public delegate* unmanaged[Cdecl, SuppressGCTransition]<in int, void> _fp;
처음 C# 9에서 함수 포인터 구문을 접했을 땐,
delegate* managed<int, int, int> p1 = null;
delegate* unmanaged[Stdcall]<int, int, int> p2 = null;
저렇게 "[", "]" 대괄호 안에 올 수 있는 것은 특별히 Calling Convention에 한해서 가능한 거라고 생각했는데요, 실제로는 마치 특성(Attribute)처럼 지정할 수 있다는 것이 흥미로웠습니다.
그런데, 저기에 지정된 Stdcall같은 타입들은 실제로 단순히 class로만 정의돼 있는 것이 맞습니다.
public class CallConvCdecl
{
public CallConvCdecl() { }
}
public class CallConvFastcall
{
public CallConvFastcall() { }
}
public class CallConvStdcall
{
public CallConvStdcall() { }
}
게다가, 함께 지정 가능한 SuppressGCTransition은 특성이 맞습니다.
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class SuppressGCTransitionAttribute : Attribute
{
public SuppressGCTransitionAttribute()
{
}
}
오호~~~ 그렇다면 아마도 마이크로소프트 측은 처음부터 저 구문에 특성을 지정할 수 있게 만들지는 않았을 거로 보이는데요, 테스트를 위해 .NET 5 프로젝트를 만들어 다음의 코드를 컴파일해 보면,
using System;
namespace ConsoleApp1
{
internal unsafe class Program
{
delegate* unmanaged[Cdecl, SuppressGCTransition]<byte*, void> _cpuIdFunc; // 컴파일 오류
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class SuppressGCTransitionAttribute : Attribute // .NET 6부터 정의됐으므로 일부러 추가
{
public SuppressGCTransitionAttribute()
{
}
}
}
이런 오류가 발생합니다.
error CS8890: Type 'CallConvSuppressGCTransition' is not defined.
즉, 이름 규칙을 "CallConv" 접두사를 붙인 것들만 허용한 것입니다. 그렇다고 해서 CallConvSuppressGCTransition 타입을 정의해도 여전히 오류는 발생합니다. 이거저거 테스트해 보면, 결국 .NET 6부터 delegate*에 지정 가능한 특성(?)은 기존에 정의된 Calling Convention 타입들과 특별히 SuppressGCTransition 특성만 지정하도록 제한돼 있는 것을 알 수 있습니다. (일례로, .NET 8.0에서도 AttributeTargets.Method인 사용자 정의 특성을 설정하면 컴파일 오류가 발생합니다.)
뭐랄까, 약간 하드 코딩을 했다는 느낌마저 드는데요, 그렇다고 해도 저런 코드를 작성하게 된 것은 환영할 만합니다.
왜냐하면, delegate* +
SuppressGCTransition 조합이야말로 닷넷 세계에서 현존하는 Native API 호출 방식 중 가장 빠른 유형이기 때문입니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]