다음을 통해 공유


함수 포인터

메모

이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 ECMA 사양에 통합될 때까지 게시됩니다.

기능 사양과 완료된 구현 간에 약간의 불일치가 있을 수 있습니다. 이러한 차이는 언어 디자인 모임(LDM) 관련노트에 기록됩니다.

사양문서에서 기능 사양을 C# 언어 표준에 채택하는 과정에 대해 자세히 알아볼 수 있습니다.

요약

이 제안은 현재 C#에서 효율적으로 또는 전혀 액세스할 수 없는 IL opcode를 노출하는 언어 구문을 제공합니다. ldftncalli. 이러한 IL opcode는 고성능 코드에서 중요할 수 있으며 개발자는 이를 액세스하는 효율적인 방법이 필요합니다.

동기

이 기능에 대한 동기와 배경은 다음 문제(기능의 잠재적 구현과 마찬가지로)에 설명되어 있습니다.

dotnet/csharplang#191

컴파일러 내장 기능 에 대한 대체 설계 제안입니다

상세 디자인

함수 포인터

언어를 사용하면 delegate* 구문을 사용하여 함수 포인터를 선언할 수 있습니다. 전체 구문은 다음 섹션에서 자세히 설명하지만 FuncAction 형식 선언에서 사용하는 구문과 유사합니다.

unsafe class Example
{
    void M(Action<int> a, delegate*<int, void> f)
    {
        a(42);
        f(42);
    }
}

이러한 형식은 ECMA-335에 설명된 대로 함수 포인터 형식을 사용하여 표시됩니다. 즉, delegate* 호출은 Invoke 메서드에서 calli을 사용하는 반면, delegate 호출은 callvirt을 사용합니다. 호출은 두 구문 모두에 대해 구문적으로 동일합니다.

메서드 포인터의 ECMA-335 정의에는 형식 서명의 일부로 호출 규칙이 포함됩니다(섹션 7.1). 기본 호출 규칙은 managed. 관리되지 않는 호출 규칙은 delegate* 구문 뒤에 unmanaged 키워드를 넣어 지정할 수 있으며, 이는 런타임 플랫폼의 기본값을 사용합니다. 그런 다음 System.Runtime.CompilerServices 네임스페이스에서 CallConv 시작하여 CallConv 접두사를 벗어나는 형식을 지정하여 특정 관리되지 않는 규칙을 unmanaged 키워드에 대한 대괄호로 지정할 수 있습니다. 이러한 형식은 프로그램의 핵심 라이브러리에서 제공해야 하며 유효한 조합 집합은 플랫폼에 따라 다릅니다.

//This method has a managed calling convention. This is the same as leaving the managed keyword off.
delegate* managed<int, int>;

// This method will be invoked using whatever the default unmanaged calling convention on the runtime
// platform is. This is platform and architecture dependent and is determined by the CLR at runtime.
delegate* unmanaged<int, int>;

// This method will be invoked using the cdecl calling convention
// Cdecl maps to System.Runtime.CompilerServices.CallConvCdecl
delegate* unmanaged[Cdecl] <int, int>;

// This method will be invoked using the stdcall calling convention, and suppresses GC transition
// Stdcall maps to System.Runtime.CompilerServices.CallConvStdcall
// SuppressGCTransition maps to System.Runtime.CompilerServices.CallConvSuppressGCTransition
delegate* unmanaged[Stdcall, SuppressGCTransition] <int, int>;

delegate* 형식 간의 변환은 호출 규칙을 포함한 서명에 따라 수행됩니다.

unsafe class Example {
    void Conversions() {
        delegate*<int, int, int> p1 = ...;
        delegate* managed<int, int, int> p2 = ...;
        delegate* unmanaged<int, int, int> p3 = ...;

        p1 = p2; // okay p1 and p2 have compatible signatures
        Console.WriteLine(p2 == p1); // True
        p2 = p3; // error: calling conventions are incompatible
    }
}

delegate* 형식은 표준 포인터 형식의 모든 기능 및 제한을 포함하는 포인터 형식입니다.

  • unsafe 컨텍스트에서만 유효합니다.
  • delegate* 매개 변수 또는 반환 형식을 포함하는 메서드는 unsafe 컨텍스트에서만 호출할 수 있습니다.
  • 이것을 object로 변환할 수 없습니다.
  • 제네릭 인수로 사용할 수 없습니다.
  • 암시적으로 delegate*을(를) void*으로 변환할 수 있습니다.
  • 명시적으로 void*delegate*로 변환할 수 있습니다.

제한:

  • 사용자 지정 특성은 delegate* 또는 해당 요소에 적용할 수 없습니다.
  • delegate* 매개 변수를 params 표시할 수 없습니다.
  • delegate* 형식에는 일반 포인터 형식의 모든 제한이 있습니다.
  • 포인터 산술 연산은 함수 포인터 형식에서 직접 수행할 수 없습니다.

함수 포인터 구문

전체 함수 포인터 구문은 다음 문법으로 표시됩니다.

pointer_type
    : ...
    | funcptr_type
    ;

funcptr_type
    : 'delegate' '*' calling_convention_specifier? '<' funcptr_parameter_list funcptr_return_type '>'
    ;

calling_convention_specifier
    : 'managed'
    | 'unmanaged' ('[' unmanaged_calling_convention ']')?
    ;

unmanaged_calling_convention
    : 'Cdecl'
    | 'Stdcall'
    | 'Thiscall'
    | 'Fastcall'
    | identifier (',' identifier)*
    ;

funptr_parameter_list
    : (funcptr_parameter ',')*
    ;

funcptr_parameter
    : funcptr_parameter_modifier? type
    ;

funcptr_return_type
    : funcptr_return_modifier? return_type
    ;

funcptr_parameter_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

funcptr_return_modifier
    : 'ref'
    | 'ref readonly'
    ;

calling_convention_specifier 제공되지 않으면 기본값은 managed. calling_convention_specifier의 정확한 메타데이터 인코딩과 unmanaged_calling_convention에서 유효한 identifier의 항목은 의 호출 규칙 메타데이터 표현에서 다룹니다.

delegate int Func1(string s);
delegate Func1 Func2(Func1 f);

// Function pointer equivalent without calling convention
delegate*<string, int>;
delegate*<delegate*<string, int>, delegate*<string, int>>;

// Function pointer equivalent with calling convention
delegate* managed<string, int>;
delegate*<delegate* managed<string, int>, delegate*<string, int>>;

함수 포인터 변환

안전하지 않은 컨텍스트에서 사용 가능한 암시적 변환 집합(암시적 변환)은 다음과 같은 암시적 포인터 변환을 포함하도록 확장됩니다.

  • 기존 변환 - (§23.5)
  • funcptr_typeF0에서 다른 funcptr_typeF1로, 다음 조건이 모두 참인 경우에만
    • F0F1은 동일한 수의 매개 변수를 가지며, F0의 각 매개 변수 D0nF1의 각 매개 변수 D1n과 동일한 ref, out또는 in 한정자를 가지고 있습니다.
    • 각 값 매개 변수(ref, out, 또는 in 한정자가 없는 매개 변수)에 대해, F0의 매개 변수 유형에서 F1의 해당 매개 변수 유형으로의 동일성 변환, 암시적 참조 변환, 또는 암시적 포인터 변환이 존재합니다.
    • ref, out또는 in 매개 변수에 대해 F0 매개 변수 형식은 F1해당 매개 변수 형식과 동일합니다.
    • 반환 형식이 값(즉, ref 또는 ref readonly아님)인 경우, 반환 형식 F1에서 반환 형식 F0로의 아이덴티티, 암시적 참조 또는 암시적 포인터 변환이 존재합니다.
    • 반환 형식이 참조(ref 또는 ref readonly)인 경우 F1 반환 형식 및 ref 한정자는 F0반환 형식 및 ref 한정자와 동일합니다.
    • F0 호출 규칙은 F1호출 규칙과 동일합니다.

대상 메서드에 대한 주소 허용

이제 메서드 그룹이 식 주소에 대한 인수로 허용됩니다. 이러한 표현의 형식은 대상 메서드와 관리되는 호출 규칙의 동일한 시그니처를 가진 delegate*가 됩니다.

unsafe class Util {
    public static void Log() { }

    void Use() {
        delegate*<void> ptr1 = &Util.Log;

        // Error: type "delegate*<void>" not compatible with "delegate*<int>";
        delegate*<int> ptr2 = &Util.Log;
   }
}

안전하지 않은 컨텍스트에서 M 메서드는 다음이 모두 true이면 F 함수 포인터 형식과 호환됩니다.

  • MF 동일한 수의 매개 변수를 가지며, M 각 매개 변수에는 F해당 매개 변수와 동일한 ref, out또는 in 한정자가 있습니다.
  • 각 값 매개 변수(매개 변수에 ref, out또는 in 한정자가 없는 경우)에 대해, M 매개 변수 형식에서 F의 해당 매개 변수 형식으로의 종류 변환, 암시적 참조 변환 또는 암시적 포인터 변환이 존재합니다.
  • ref, out또는 in 매개 변수에 대해 M 매개 변수 형식은 F해당 매개 변수 형식과 동일합니다.
  • 반환 유형이 값에 의한 경우 (ref 또는 ref readonly이 없는 경우), 식별자, 암시적 참조 또는 암시적 포인터 변환이 F의 반환 유형에서 M의 반환 유형으로 존재합니다.
  • 반환 형식이 참조(ref 또는 ref readonly)인 경우 F 반환 형식 및 ref 한정자는 M반환 형식 및 ref 한정자와 동일합니다.
  • M 호출 규칙은 F호출 규칙과 동일합니다. 여기에는 호출 규칙 비트와 관리되지 않는 식별자에 지정된 호출 규칙 플래그가 모두 포함됩니다.
  • M 정적 메서드입니다.

안전하지 않은 컨텍스트에서는, 대상이 메서드 그룹 E인 주소 식에서 호환 가능한 함수 포인터 형식 F로의 암시적 변환이 존재합니다. 이는 EF의 매개 변수 형식 및 한정자를 사용하여 구성된 인수 목록에 정규 형식으로 적용할 수 있는 메서드가 하나 이상 포함되어 있는 경우입니다.

  • 다음과 같이 수정된 E(A) 양식의 메서드 호출에 해당하는 단일 메서드 M 선택됩니다.
    • A 인수 목록은 식 목록으로, 각각이 변수로 분류되며, 해당 Ffuncptr_parameter_list의 형식과 한정자(ref, out또는 in)로 분류됩니다.
    • 후보 메서드는 일반 형식에서 적용할 수 있는 메서드일 뿐 확장된 형식에 적용할 수 있는 메서드는 아닙니다.
    • 후보 메서드는 정적 메서드일 뿐입니다.
  • 오버로드 확인 알고리즘에서 오류가 발생하면 컴파일 시간 오류가 발생합니다. 알고리즘은 그렇지 않으면 F과 동일한 수의 매개변수를 가진 단일 최상의 메서드 M를 생성하고, 이 경우 변환은 존재하는 것으로 간주됩니다.
  • 선택한 메서드 M는 함수 포인터 형식 F과(위에서 정의한 대로) 호환되어야 합니다. 그렇지 않으면 컴파일 시간 오류가 발생합니다.
  • 변환 결과는 F형식의 함수 포인터입니다.

즉, 개발자는 주소 지정 연산자와 함께 작동하도록 오버로드 해석 규칙에 의존할 수 있습니다.

unsafe class Util {
    public static void Log() { }
    public static void Log(string p1) { }
    public static void Log(int i) { }

    void Use() {
        delegate*<void> a1 = &Log; // Log()
        delegate*<int, void> a2 = &Log; // Log(int i)

        // Error: ambiguous conversion from method group Log to "void*"
        void* v = &Log;
    }
}

주소 연산자는 ldftn 명령을 사용하여 구현됩니다.

이 기능의 제한 사항:

  • static표시된 메서드에만 적용됩니다.
  • &에서static 범위에 속하지 않는 함수를 사용할 수 없습니다. 이러한 메서드의 구현 세부 정보는 언어에 의해 의도적으로 지정되지 않습니다. 여기에는 정적 및 인스턴스인지 또는 정확히 어떤 서명으로 내보내는지 여부가 포함됩니다.

함수 포인터 유형의 연산자

표현에 관한 안전하지 않은 코드 섹션은 다음과 같이 수정됩니다.

안전하지 않은 컨텍스트에서는 _funcptr_type_s가 아닌 모든 _pointer_type_s에 대해 여러 구성 요소를 사용할 수 있습니다.

  • * 연산자는 포인터 간접 참조(§23.6.2)를 수행하는 데 사용할 수 있습니다.
  • -> 연산자는 포인터(§23.6.3)를 통해 구조체의 멤버에 액세스하는 데 사용할 수 있습니다.
  • [] 연산자는 포인터(§23.6.4)를 인덱싱하는 데 사용할 수 있습니다.
  • & 연산자는 변수의 주소를 가져오는 데 사용할 수 있습니다(§23.6.5).
  • ++-- 연산자는 포인터를 증가시키고 감소하는 데 사용할 수 있습니다(§23.6.6).
  • +- 연산자는 포인터 산술 연산을 수행하는 데 사용할 수 있습니다(§23.6.7).
  • ==, !=, <, >, <==> 연산자를 사용하여 포인터(§23.6.8)를 비교할 수 있습니다.
  • stackalloc 연산자는 호출 스택(§23.8)에서 메모리를 할당하는 데 사용할 수 있습니다.
  • fixed 문을 사용하여 해당 주소를 가져올 수 있도록 변수를 일시적으로 수정할 수 있습니다(§23.7).

위험한 환경에서는 모든 _funcptr_type_s를 작동시키기 위한 여러 구조들이 제공됩니다.

또한 Pointers in expressions의 모든 섹션을 함수 포인터 유형을 금지하도록 수정하며, Pointer comparisonThe sizeof operator는 예외입니다.

향상된 함수 멤버

§12.6.4.3 더 나은 함수 멤버 에 다음 줄을 포함하도록 변경됩니다.

delegate* void* 보다 구체적입니다.

즉, void*delegate*에 대해 오버로드가 가능하며, 여전히 주소-연산자를 현명하게 사용할 수 있습니다.

타입 추론

안전하지 않은 코드에서는 형식 유추 알고리즘을 다음과 같이 변경합니다.

입력 형식

§12.6.3.4

다음이 추가됩니다.

만약 E이 메서드 그룹의 주소이고 T이 함수 포인터 형식인 경우, T의 모든 매개변수 형식은 T형식의 E 입력 형식입니다.

출력 형식

§12.6.3.5

다음이 추가됩니다.

E이 주소 지정 메서드 그룹이고 T이 함수 포인터 형식인 경우, T의 반환 형식은 T유형의 E 출력 형식입니다.

출력 형식 유추

§12.6.3.7

글머리 기호 2와 3 사이에 다음 글머리 기호가 추가됩니다.

  • E이(가) 주소 참조를 위한 메서드 그룹이고, T이(가) 매개변수 형식이 T1...Tk이고 반환 형식이 Tb인 함수 포인터 형식인 경우, E에 대한 T1..Tk 형식으로의 오버로드 해결 결과가 반환 형식이 U인 단일 메서드를 산출하면, U에서 Tb으로의 하한 유추이 이루어집니다.

식에서 더 나은 변환 과정

§12.6.4.5

다음 하위 글머리 기호는 목록 2의 사례로 추가됩니다.

  • V는 함수 포인터 형식의 delegate*<V2..Vk, V1>이고, U는 함수 포인터 형식의 delegate*<U2..Uk, U1>이며, V의 호출 규약은 U와 동일하고, Vi의 참조 여부는 Ui과 같습니다.

하한 유추

§12.6.3.10

다음 사례가 항목 3에 추가되었습니다.

  • V는 함수 포인터 유형 delegate*<V2..Vk, V1>이며, delegate*<U2..Uk, U1>라는 함수 포인터 유형이 있어서, Udelegate*<U2..Uk, U1>와 동일하고, V의 호출 규약이 U와 동일하며, Vi의 참조 특성이 Ui와 동일합니다.

Ui에서 Vi로의 유추 첫 번째 항목은 다음으로 수정됩니다.

  • U가 함수 포인터 형식이 아니고 Ui이 참조 형식으로 알려져 있지 않은 경우, 또는 U가 함수 포인터 형식이며 Ui이 함수 포인터 형식이나 참조 형식으로 알려져 있지 않은 경우, 정확한 추론가 이루어집니다.

그런 다음 유추의 세 번째 글머리 기호가 Ui에서 Vi로 추가됩니다.

  • 그렇지 않고 Vdelegate*<V2..Vk, V1>와 같다면, 유추는 delegate*<V2..Vk, V1>의 i번째 매개 변수에 따라 달라집니다.
    • V1인 경우:
      • 반환이 값 기준이면 하한 유추 만들어집니다.
      • 반환이 참조로 이루어지면 정확한 유추 이루어집니다.
    • V2에서 Vk까지의 경우:
      • 매개 변수가 값으로 있으면 상한 유추 만들어집니다.
      • 매개 변수가 참조로 있으면 정확한 유추 만들어집니다.

상한 추론

§12.6.3.11

다음 사례는 두 번째 항목에 추가됩니다.

  • U은 함수 포인터 형식이고, delegate*<U2..Uk, U1>은 함수 포인터 형식이며 Vdelegate*<V2..Vk, V1>과 동일한 함수 포인터 형식이며, U의 호출 규칙은 V와 동일하며, Ui의 참조성은 Vi과 동일합니다.

Ui에서 Vi로의 유추의 첫 번째 글머리 기호는 다음과 같이 수정됩니다.

  • U가 함수 포인터 형식이 아니고 Ui이 참조 형식으로 알려져 있지 않거나, 또는 U가 함수 포인터 형식이면서 Ui이 함수 포인터 형식이나 참조 형식으로 알려져 있지 않으면, 정확한 유추가 이루어집니다.

그런 다음 UiVi유추의 세 번째 글머리 기호 다음에 추가됩니다.

  • 그렇지 않다면, Udelegate*<U2..Uk, U1>인 경우 유추는 delegate*<U2..Uk, U1>의 i번째 매개변수에 달려 있습니다.
    • U1인 경우:
      • 반환이 값으로 되면 상한 유추 발생합니다.
      • 반환이 참조를 통해 이루어지면 정확한 유추가 수행됩니다.
    • "U2부터 Uk까지인 경우:"
      • 매개 변수가 값으로 있으면 하한 유추 만들어집니다.
      • 매개 변수가 참조로 전달되면 정확한 유추 수행됩니다.

in, outref readonly 매개 변수 및 반환 형식의 메타데이터 표현

함수 포인터 시그니처에는 매개변수 플래그 위치가 없기 때문에, 매개변수와 반환 유형이 in, out, 또는 ref readonly로 인코딩되는지를 modreqs를 사용하여 지정해야 합니다.

in

매개변수 또는 반환 유형의 ref 지정자에 modreq로 적용된 System.Runtime.InteropServices.InAttribute를 재사용하여 다음과 같은 의미를 가집니다.

  • 매개 변수 ref 지정자에 적용된 경우 이 매개 변수는 in처리됩니다.
  • 반환 형식 ref 지정자에 적용된 경우 반환 형식은 ref readonly처리됩니다.

out

우리는 매개 변수가 out 매개 변수임을 나타내기 위해 매개 변수 유형의 ref 지정자에 modreq로 적용된 System.Runtime.InteropServices.OutAttribute을 사용합니다.

오류

  • OutAttribute을(를) 반환 유형에 modreq로 적용하는 것은 오류입니다.
  • InAttributeOutAttribute 모두 매개 변수 형식에 modreq로 적용하는 것은 오류입니다.
  • modopt를 통해 둘 중 하나라도 지정된 경우 무시됩니다.

호출 규칙의 메타데이터 표현

호출 규칙은 서명 내 CallKind 플래그와 서명 시작 시 0개 이상의 modopt조합으로 메타데이터의 메서드 서명에 인코딩됩니다. ECMA-335는 현재 CallKind 플래그에서 다음 요소를 선언합니다.

CallKind
   : default
   | unmanaged cdecl
   | unmanaged fastcall
   | unmanaged thiscall
   | unmanaged stdcall
   | varargs
   ;

이 중 C#의 함수 포인터는 varargs제외한 모든 포인터를 지원합니다.

또한 런타임과 335도 새 플랫폼에 새 CallKind를 포함하도록 차차 업데이트될 예정입니다. 현재 정식 이름은 없지만 이 문서에서는 unmanaged ext 자리 표시자로 사용하여 새로운 확장 가능한 호출 규칙 형식을 나타냅니다. modopt이 없을 때 unmanaged extunmanaged 대괄호 없이 플랫폼 기본 호출 규칙입니다.

calling_convention_specifier을(를) CallKind로 매핑

생략되거나 managed로 지정된 calling_convention_specifierdefaultCallKind에 매핑됩니다. UnmanagedCallersOnly특성이 지정되지 않은 어떤 메서드의 기본 CallKind입니다.

C#은 ECMA 335의 특정 기존 비관리 CallKind에 매핑되는 4개의 특수 식별자를 인식합니다. 이 매핑이 발생하려면 다른 식별자가 없는 자체 식별자를 지정해야 하며 이 요구 사항은 unmanaged_calling_convention사양으로 인코딩됩니다. 이러한 식별자는 Cdecl, Thiscall, Stdcall, Fastcall이며, 각각 unmanaged cdecl, unmanaged thiscall, unmanaged stdcall, unmanaged fastcall에 해당합니다. 둘 이상의 identifer 지정되었거나 단일 identifier 특별히 인식된 식별자가 아닌 경우 다음 규칙을 사용하여 식별자에 대해 특별한 이름 조회를 수행합니다.

  • 우리는 문자열 CallConvidentifier 앞에 추가합니다.
  • System.Runtime.CompilerServices 네임스페이스에 정의된 형식만 살펴보겠습니다.
  • 애플리케이션의 핵심 라이브러리에 정의된 형식만 살펴봅니다. 이 라이브러리는 System.Object을 정의하고 종속성이 없습니다.
  • 공용 형식만 살펴봅니다.

모든 identifier조회가 unmanaged_calling_convention내에서 성공하면 CallKindunmanaged ext로 인코딩하고, 함수 포인터 서명의 시작 부분에 있는 modopt집합에서 확인된 각 형식을 인코딩합니다. 참고로, 이러한 규칙은 사용자가 identifierCallConv을 접두사로 붙일 수 없다는 것을 의미합니다. 그렇게 하면 CallConvCallConvVectorCall를 조회해야 하기 때문입니다.

메타데이터를 해석할 때 먼저 CallKind살펴보겠습니다. unmanaged ext이외의 경우 호출 규칙을 결정하기 위해 반환 형식의 모든 modopt무시하고 CallKind만 사용합니다. CallKindunmanaged ext일 경우, 함수 포인터 형식의 시작 부분에 있는 수정 옵션을 살펴보고 다음의 요건을 충족하는 모든 형식을 통합합니다.

  • 다른 라이브러리를 참조하지 않고 System.Object정의하는 라이브러리인 핵심 라이브러리에 정의됩니다.
  • 형식은 System.Runtime.CompilerServices 네임스페이스에 정의됩니다.
  • 형식은 접두사 CallConv으로 시작됩니다.
  • 형식이 public입니다.

소스에서 함수 포인터 형식을 정의할 때, unmanaged_calling_convention 내의 identifier에서 조회를 수행하여 찾아야 하는 형식을 나타냅니다.

대상 런타임이 기능을 지원하지 않는 경우 CallKindunmanaged ext 함수 포인터를 사용하려고 시도하는 것은 오류입니다. 이는 System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind 상수의 존재를 확인하여 결정됩니다. 이 상수가 있는 경우 런타임은 기능을 지원하는 것으로 간주됩니다.

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute 특정 호출 규칙을 사용하여 메서드를 호출해야 함을 나타내기 위해 CLR에서 사용하는 특성입니다. 이 때문에 속성 작업에 대해 다음과 같은 지원을 제공합니다.

  • C#에서 이 특성으로 주석이 추가된 메서드를 직접 호출하는 것은 오류입니다. 사용자는 메서드에 대한 함수 포인터를 가져온 다음 해당 포인터를 호출해야 합니다.
  • 일반 정적 메서드 또는 일반 정적 로컬 함수 이외의 다른 항목에 특성을 적용하는 것은 오류입니다. C# 컴파일러는 이 특성으로 인해 언어에서 지원되지 않는 것으로 메타데이터에서 가져온 비정적 또는 정적 비일반 메서드를 표시합니다.
  • 특성으로 표시된 메서드에 unmanaged_type아닌 매개 변수 또는 반환 형식이 있는 것은 오류입니다.
  • 형식 매개 변수가 unmanaged으로 제한되더라도, 특성으로 표시된 메서드에 형식 매개 변수가 있으면 오류입니다.
  • 제네릭 형식의 메서드가 특성으로 표시되는 것은 오류입니다.
  • 특성으로 표시된 메서드를 대리자 형식으로 변환하는 것은 오류입니다.
  • 메타데이터에서 규칙 modopt호출에 대한 요구 사항을 충족하지 않는 UnmanagedCallersOnly.CallConvs 형식을 지정하는 것은 오류입니다.

유효한 UnmanagedCallersOnly 특성으로 표시된 메서드의 호출 규약을 결정할 때, 컴파일러는 CallConvs 속성에 지정된 형식들을 검사하여 호출 규약을 결정하는 데 사용해야 하는 효과적인 CallKindmodopt를 확인합니다.

  • 형식이 명시되지 않으면 CallKind는 함수 포인터 형식의 시작 부분에 호출 규칙 modopt없이 unmanaged ext로 처리됩니다.
  • 지정된 형식이 하나 있고 해당 형식의 이름이 CallConvCdecl, CallConvThiscall, CallConvStdcall또는 CallConvFastcall일 경우, CallKind는 각각 unmanaged cdecl, unmanaged thiscall, unmanaged stdcall또는 unmanaged fastcall로 처리되며, 함수 포인터 형식의 시작 부분에는 호출 규칙 modopt이 없습니다.
  • 여러 형식이 지정되었거나 단일 형식이 위에서 특별히 강조된 형식 중 하나가 아닌 경우, CallKindunmanaged ext로 취급되고, 지정된 형식의 합집합은 함수 포인터 형식의 시작 부분에서 modopt로 고려됩니다.

그런 다음 컴파일러는 이 유효 CallKindmodopt 컬렉션을 살펴보고 일반 메타데이터 규칙을 사용하여 함수 포인터 형식의 최종 호출 규칙을 결정합니다.

질문 열기

unmanaged ext에 대한 런타임 지원 감지

https://github.com/dotnet/runtime/issues/38135 이 플래그를 추가하는 것을 추적합니다. 검토 피드백에 따라, 문제에 지정된 속성을 사용하거나, UnmanagedCallersOnlyAttribute의 존재를 런타임들이 unmanaged ext을 지원하는지 여부를 결정하는 플래그로 사용할 것입니다.

고려 사항

인스턴스 메서드 허용

이 제안은 EXPLICITTHIS CLI 호출 규칙(C# 코드에서 instance 명명됨)을 활용하여 인스턴스 메서드를 지원하도록 확장될 수 있습니다. 이 형태의 CLI 함수 포인터는 this 매개 변수를 함수 포인터 구문의 명시적 첫 번째 매개 변수로 사용합니다.

unsafe class Instance {
    void Use() {
        delegate* instance<Instance, string> f = &ToString;
        f(this);
    }
}

이것은 건전하지만 제안에 어떤 복잡성을 더합니다. 특히, 호출 규칙 instancemanaged에 따라 서로 다른 함수 포인터는 두 경우 모두 동일한 C# 서명으로 관리되는 메서드를 호출하는 데 사용되더라도 호환되지 않습니다. 값진 경우를 고려할 때마다 활용할 수 있는 간단한 해결책은 static 로컬 함수를 사용하는 것이었습니다.

unsafe class Instance {
    void Use() {
        static string toString(Instance i) => i.ToString();
        delegate*<Instance, string> f = &toString;
        f(this);
    }
}

선언 시 안전하지 않은 요구 사항을 요구하지 않음

delegate*을 사용할 때마다 unsafe를 요구하는 대신, 메서드 그룹이 delegate*로 변환되는 지점에서만 unsafe가 필요합니다. 여기서 핵심 안전 문제가 발생합니다(값이 활성 상태인 동안 포함 어셈블리를 언로드할 수 없음을 알고 있음). 다른 위치에 unsafe 조건을 요구하는 것은 과도한 것으로 볼 수 있습니다.

디자인이 원래 의도된 방식입니다. 그러나 생성된 언어 규칙은 매우 어색하게 느껴졌다. 포인터 값임이 드러나는 것은 숨길 수 없으며, unsafe 키워드 없이도 계속 드러났습니다. 예를 들어 object 변환을 허용할 수 없으며 class멤버가 될 수 없습니다. C# 디자인은 모든 포인터 사용에 대해 unsafe 요구하므로 이 디자인은 이를 따릅니다.

개발자는 일반 포인터 유형과 마찬가지로 delegate* 값 위에 안전한 래퍼를 표시할 수 있는 기능을 계속 가질 것입니다. 고려:

unsafe struct Action {
    delegate*<void> _ptr;

    Action(delegate*<void> ptr) => _ptr = ptr;
    public void Invoke() => _ptr();
}

대리자 사용

delegate*새 구문 요소를 사용하는 대신, 형식 다음에 * 기존 delegate 형식을 사용하기만 하면 됩니다.

Func<object, object, bool>* ptr = &object.ReferenceEquals;

호출 규칙 처리는 CallingConvention 값을 지정하는 속성을 delegate 형식에 추가하여 처리할 수 있습니다. 특성이 없을 경우, 관리되는 호출 규칙을 나타냅니다.

IL에서 이를 인코딩하는 것은 문제가 됩니다. 기본 값은 포인터로 표시되어야 하지만 다음도 수행해야 합니다.

  1. 함수 포인터 유형이 서로 다른 오버로드를 허용하기 위해 고유한 유형을 사용하십시오.
  2. OHI 용도에서 어셈블리 경계를 넘어 동등하게 유지됩니다.

마지막 점은 특히 문제가 됩니다. 즉, Func<int>*을 사용하는 모든 어셈블리는, 비록 Func<int>*이 제어하지 않는 어셈블리에 정의되어 있더라도, 메타데이터에서 동일한 형식을 인코딩해야 합니다. 또한 mscorlib가 아닌 어셈블리의 System.Func<T> 이름으로 정의된 다른 형식은 mscorlib에 정의된 버전과 달라야 합니다.

탐색된 한 가지 옵션은 mod_req(Func<int>) void*같은 포인터를 내보내는 것이었습니다. mod_reqTypeSpec에 바인딩할 수 없기 때문에 제네릭 인스턴스화를 대상으로 지정할 수 없어 작동하지 않습니다.

명명된 함수 포인터

함수 포인터 구문은 특히 중첩된 함수 포인터와 같은 복잡한 경우 번거로울 수 있습니다. 개발자가 매번 서명을 입력하는 대신, 언어에서 delegate에서처럼 함수 포인터의 명명된 선언을 허용할 수 있습니다.

func* void Action();

unsafe class NamedExample {
    void M(Action a) {
        a();
    }
}

여기서 문제의 일부는 기본 CLI 기본 형식에 이름이 없으므로 이는 순전히 C# 발명이며 사용하도록 설정하려면 약간의 메타데이터 작업이 필요합니다. 그것은 가능하지만 상당한 양의 일이 필요합니다. 사실상 C#에는 이러한 이름을 위해 형식 정의 테이블의 동반자가 필요합니다.

또한 명명된 함수 포인터에 대한 인수를 검사할 때 여러 다른 시나리오에 동일하게 잘 적용할 수 있음을 발견했습니다. 예를 들어 모든 경우에 전체 서명을 입력해야 하는 필요를 줄이기 위해 명명된 튜플을 선언하면 편리합니다.

(int x, int y) Point;

class NamedTupleExample {
    void M(Point p) {
        Console.WriteLine(p.x);
    }
}

토론 후에는 delegate* 형식의 명명된 선언을 허용하지 않기로 결정했습니다. 고객 사용 피드백에 따라 이에 대한 상당한 필요성이 있는 경우 함수 포인터, 튜플, 제네릭 등에 적합한 명명 솔루션을 조사합니다. 이는 언어의 전체 typedef 지원과 같은 다른 제안과 형태가 유사할 수 있습니다.

향후 고려 사항

정적 대리자

이는 제안로, static 멤버만 참조할 수 있는 delegate 형식의 선언을 허용하는 것을 의미합니다. 이러한 delegate 인스턴스는 할당 없이 성능에 민감한 시나리오에서 더 뛰어난 효율성을 제공하는 장점이 있습니다.

함수 포인터 기능이 구현되면 static delegate 제안이 종료될 수 있습니다. 이 기능의 제안된 이점은 할당이 없는 특성입니다. 그러나 최근 조사 결과 어셈블리 언로드로 인해 달성할 수 없는 것으로 나타났습니다. 어셈블리가 그 아래에서 언로드되지 않도록 하려면 static delegate 참조하는 메서드에 대한 강력한 핸들이 있어야 합니다.

모든 static delegate 인스턴스를 유지하려면 제안의 목표에 반하는 새 핸들을 할당해야 합니다. 호출 사이트당 단일 할당으로 감가상각할 수 있는 몇 가지 설계가 있었지만, 약간 복잡하여 그만한 가치가 없어 보였습니다.

즉, 개발자는 사실상 다음의 절충안 중 하나를 결정해야 합니다.

  1. 어셈블리 언로드 시 안전성: 할당이 필요하므로 delegate 이미 충분한 옵션입니다.
  2. 조립 해제 시 안전성이 없으니 delegate*을 사용하세요. 사용 목적에 맞게 unsafe 컨텍스트 외부에서 쓸 수 있도록, struct으로 감싸서 코드의 나머지 부분에 활용할 수 있습니다.