함수 포인터
메모
이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 ECMA 사양에 통합될 때까지 게시됩니다.
기능 사양과 완료된 구현 간에 약간의 불일치가 있을 수 있습니다. 이러한 차이는 언어 디자인 모임(LDM) 관련노트에 기록됩니다.
사양문서에서 기능 사양을 C# 언어 표준에 채택하는 과정에 대해 자세히 알아볼 수 있습니다.
요약
이 제안은 현재 C#에서 효율적으로 또는 전혀 액세스할 수 없는 IL opcode를 노출하는 언어 구문을 제공합니다. ldftn
및 calli
. 이러한 IL opcode는 고성능 코드에서 중요할 수 있으며 개발자는 이를 액세스하는 효율적인 방법이 필요합니다.
동기
이 기능에 대한 동기와 배경은 다음 문제(기능의 잠재적 구현과 마찬가지로)에 설명되어 있습니다.
컴파일러 내장 기능 에 대한 대체 설계 제안입니다
상세 디자인
함수 포인터
언어를 사용하면 delegate*
구문을 사용하여 함수 포인터를 선언할 수 있습니다. 전체 구문은 다음 섹션에서 자세히 설명하지만 Func
및 Action
형식 선언에서 사용하는 구문과 유사합니다.
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_type
F0
에서 다른 funcptr_typeF1
로, 다음 조건이 모두 참인 경우에만-
F0
및F1
은 동일한 수의 매개 변수를 가지며,F0
의 각 매개 변수D0n
는F1
의 각 매개 변수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
함수 포인터 형식과 호환됩니다.
-
M
및F
동일한 수의 매개 변수를 가지며,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
로의 암시적 변환이 존재합니다. 이는 E
에 F
의 매개 변수 형식 및 한정자를 사용하여 구성된 인수 목록에 정규 형식으로 적용할 수 있는 메서드가 하나 이상 포함되어 있는 경우입니다.
- 다음과 같이 수정된
E(A)
양식의 메서드 호출에 해당하는 단일 메서드M
선택됩니다.-
A
인수 목록은 식 목록으로, 각각이 변수로 분류되며, 해당F
의 funcptr_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를 작동시키기 위한 여러 구조들이 제공됩니다.
&
연산자를 사용하여 정적 메서드의 주소를 가져올 수 있습니다(대상 메서드에 대한 주소 허용)==
,!=
,<
,>
,<=
및=>
연산자를 사용하여 포인터(§23.6.8)를 비교할 수 있습니다.
또한 Pointers in expressions
의 모든 섹션을 함수 포인터 유형을 금지하도록 수정하며, Pointer comparison
및 The sizeof operator
는 예외입니다.
향상된 함수 멤버
§12.6.4.3 더 나은 함수 멤버 에 다음 줄을 포함하도록 변경됩니다.
delegate*
void*
보다 구체적입니다.
즉, void*
및 delegate*
에 대해 오버로드가 가능하며, 여전히 주소-연산자를 현명하게 사용할 수 있습니다.
타입 추론
안전하지 않은 코드에서는 형식 유추 알고리즘을 다음과 같이 변경합니다.
입력 형식
다음이 추가됩니다.
만약
E
이 메서드 그룹의 주소이고T
이 함수 포인터 형식인 경우,T
의 모든 매개변수 형식은T
형식의E
입력 형식입니다.
출력 형식
다음이 추가됩니다.
E
이 주소 지정 메서드 그룹이고T
이 함수 포인터 형식인 경우,T
의 반환 형식은T
유형의E
출력 형식입니다.
출력 형식 유추
글머리 기호 2와 3 사이에 다음 글머리 기호가 추가됩니다.
E
이(가) 주소 참조를 위한 메서드 그룹이고,T
이(가) 매개변수 형식이T1...Tk
이고 반환 형식이Tb
인 함수 포인터 형식인 경우,E
에 대한T1..Tk
형식으로의 오버로드 해결 결과가 반환 형식이U
인 단일 메서드를 산출하면,U
에서Tb
으로의 하한 유추이 이루어집니다.
식에서 더 나은 변환 과정
다음 하위 글머리 기호는 목록 2의 사례로 추가됩니다.
V
는 함수 포인터 형식의delegate*<V2..Vk, V1>
이고,U
는 함수 포인터 형식의delegate*<U2..Uk, U1>
이며,V
의 호출 규약은U
와 동일하고,Vi
의 참조 여부는Ui
과 같습니다.
하한 유추
다음 사례가 항목 3에 추가되었습니다.
V
는 함수 포인터 유형delegate*<V2..Vk, V1>
이며,delegate*<U2..Uk, U1>
라는 함수 포인터 유형이 있어서,U
이delegate*<U2..Uk, U1>
와 동일하고,V
의 호출 규약이U
와 동일하며,Vi
의 참조 특성이Ui
와 동일합니다.
Ui
에서 Vi
로의 유추 첫 번째 항목은 다음으로 수정됩니다.
U
가 함수 포인터 형식이 아니고Ui
이 참조 형식으로 알려져 있지 않은 경우, 또는U
가 함수 포인터 형식이며Ui
이 함수 포인터 형식이나 참조 형식으로 알려져 있지 않은 경우, 정확한 추론가 이루어집니다.
그런 다음 유추의 세 번째 글머리 기호가 Ui
에서 Vi
로 추가됩니다.
- 그렇지 않고
V
이delegate*<V2..Vk, V1>
와 같다면, 유추는delegate*<V2..Vk, V1>
의 i번째 매개 변수에 따라 달라집니다.
- V1인 경우:
- 반환이 값 기준이면 하한 유추 만들어집니다.
- 반환이 참조로 이루어지면 정확한 유추 이루어집니다.
- V2에서 Vk까지의 경우:
- 매개 변수가 값으로 있으면 상한 유추 만들어집니다.
- 매개 변수가 참조로 있으면 정확한 유추 만들어집니다.
상한 추론
다음 사례는 두 번째 항목에 추가됩니다.
U
은 함수 포인터 형식이고,delegate*<U2..Uk, U1>
은 함수 포인터 형식이며V
는delegate*<V2..Vk, V1>
과 동일한 함수 포인터 형식이며,U
의 호출 규칙은V
와 동일하며,Ui
의 참조성은Vi
과 동일합니다.
Ui
에서 Vi
로의 유추의 첫 번째 글머리 기호는 다음과 같이 수정됩니다.
U
가 함수 포인터 형식이 아니고Ui
이 참조 형식으로 알려져 있지 않거나, 또는U
가 함수 포인터 형식이면서Ui
이 함수 포인터 형식이나 참조 형식으로 알려져 있지 않으면, 정확한 유추가 이루어집니다.
그런 다음 Ui
Vi
유추의 세 번째 글머리 기호 다음에 추가됩니다.
- 그렇지 않다면,
U
이delegate*<U2..Uk, U1>
인 경우 유추는delegate*<U2..Uk, U1>
의 i번째 매개변수에 달려 있습니다.
- U1인 경우:
- 반환이 값으로 되면 상한 유추 발생합니다.
- 반환이 참조를 통해 이루어지면 정확한 유추가 수행됩니다.
- "U2부터 Uk까지인 경우:"
- 매개 변수가 값으로 있으면 하한 유추 만들어집니다.
- 매개 변수가 참조로 전달되면 정확한 유추 수행됩니다.
in
, out
및 ref 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로 적용하는 것은 오류입니다. -
InAttribute
및OutAttribute
모두 매개 변수 형식에 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 ext
은 unmanaged
대괄호 없이 플랫폼 기본 호출 규칙입니다.
calling_convention_specifier
을(를) CallKind
로 매핑
생략되거나 managed
로 지정된 calling_convention_specifier
는 default
CallKind
에 매핑됩니다.
UnmanagedCallersOnly
특성이 지정되지 않은 어떤 메서드의 기본 CallKind
입니다.
C#은 ECMA 335의 특정 기존 비관리 CallKind
에 매핑되는 4개의 특수 식별자를 인식합니다. 이 매핑이 발생하려면 다른 식별자가 없는 자체 식별자를 지정해야 하며 이 요구 사항은 unmanaged_calling_convention
사양으로 인코딩됩니다. 이러한 식별자는 Cdecl
, Thiscall
, Stdcall
, Fastcall
이며, 각각 unmanaged cdecl
, unmanaged thiscall
, unmanaged stdcall
, unmanaged fastcall
에 해당합니다. 둘 이상의 identifer
지정되었거나 단일 identifier
특별히 인식된 식별자가 아닌 경우 다음 규칙을 사용하여 식별자에 대해 특별한 이름 조회를 수행합니다.
- 우리는 문자열
CallConv
을identifier
앞에 추가합니다. -
System.Runtime.CompilerServices
네임스페이스에 정의된 형식만 살펴보겠습니다. - 애플리케이션의 핵심 라이브러리에 정의된 형식만 살펴봅니다. 이 라이브러리는
System.Object
을 정의하고 종속성이 없습니다. - 공용 형식만 살펴봅니다.
모든 identifier
조회가 unmanaged_calling_convention
내에서 성공하면 CallKind
를 unmanaged ext
로 인코딩하고, 함수 포인터 서명의 시작 부분에 있는 modopt
집합에서 확인된 각 형식을 인코딩합니다. 참고로, 이러한 규칙은 사용자가 identifier
에 CallConv
을 접두사로 붙일 수 없다는 것을 의미합니다. 그렇게 하면 CallConvCallConvVectorCall
를 조회해야 하기 때문입니다.
메타데이터를 해석할 때 먼저 CallKind
살펴보겠습니다.
unmanaged ext
이외의 경우 호출 규칙을 결정하기 위해 반환 형식의 모든 modopt
무시하고 CallKind
만 사용합니다.
CallKind
가 unmanaged ext
일 경우, 함수 포인터 형식의 시작 부분에 있는 수정 옵션을 살펴보고 다음의 요건을 충족하는 모든 형식을 통합합니다.
- 다른 라이브러리를 참조하지 않고
System.Object
정의하는 라이브러리인 핵심 라이브러리에 정의됩니다. - 형식은
System.Runtime.CompilerServices
네임스페이스에 정의됩니다. - 형식은 접두사
CallConv
으로 시작됩니다. - 형식이 public입니다.
소스에서 함수 포인터 형식을 정의할 때, unmanaged_calling_convention
내의 identifier
에서 조회를 수행하여 찾아야 하는 형식을 나타냅니다.
대상 런타임이 기능을 지원하지 않는 경우 CallKind
의 unmanaged 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
속성에 지정된 형식들을 검사하여 호출 규약을 결정하는 데 사용해야 하는 효과적인 CallKind
및 modopt
를 확인합니다.
- 형식이 명시되지 않으면
CallKind
는 함수 포인터 형식의 시작 부분에 호출 규칙modopt
없이unmanaged ext
로 처리됩니다. - 지정된 형식이 하나 있고 해당 형식의 이름이
CallConvCdecl
,CallConvThiscall
,CallConvStdcall
또는CallConvFastcall
일 경우,CallKind
는 각각unmanaged cdecl
,unmanaged thiscall
,unmanaged stdcall
또는unmanaged fastcall
로 처리되며, 함수 포인터 형식의 시작 부분에는 호출 규칙modopt
이 없습니다. - 여러 형식이 지정되었거나 단일 형식이 위에서 특별히 강조된 형식 중 하나가 아닌 경우,
CallKind
은unmanaged ext
로 취급되고, 지정된 형식의 합집합은 함수 포인터 형식의 시작 부분에서modopt
로 고려됩니다.
그런 다음 컴파일러는 이 유효 CallKind
및 modopt
컬렉션을 살펴보고 일반 메타데이터 규칙을 사용하여 함수 포인터 형식의 최종 호출 규칙을 결정합니다.
질문 열기
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);
}
}
이것은 건전하지만 제안에 어떤 복잡성을 더합니다. 특히, 호출 규칙 instance
및 managed
에 따라 서로 다른 함수 포인터는 두 경우 모두 동일한 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에서 이를 인코딩하는 것은 문제가 됩니다. 기본 값은 포인터로 표시되어야 하지만 다음도 수행해야 합니다.
- 함수 포인터 유형이 서로 다른 오버로드를 허용하기 위해 고유한 유형을 사용하십시오.
- OHI 용도에서 어셈블리 경계를 넘어 동등하게 유지됩니다.
마지막 점은 특히 문제가 됩니다. 즉, Func<int>*
을 사용하는 모든 어셈블리는, 비록 Func<int>*
이 제어하지 않는 어셈블리에 정의되어 있더라도, 메타데이터에서 동일한 형식을 인코딩해야 합니다.
또한 mscorlib가 아닌 어셈블리의 System.Func<T>
이름으로 정의된 다른 형식은 mscorlib에 정의된 버전과 달라야 합니다.
탐색된 한 가지 옵션은 mod_req(Func<int>) void*
같은 포인터를 내보내는 것이었습니다.
mod_req
는 TypeSpec
에 바인딩할 수 없기 때문에 제네릭 인스턴스화를 대상으로 지정할 수 없어 작동하지 않습니다.
명명된 함수 포인터
함수 포인터 구문은 특히 중첩된 함수 포인터와 같은 복잡한 경우 번거로울 수 있습니다. 개발자가 매번 서명을 입력하는 대신, 언어에서 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
인스턴스를 유지하려면 제안의 목표에 반하는 새 핸들을 할당해야 합니다. 호출 사이트당 단일 할당으로 감가상각할 수 있는 몇 가지 설계가 있었지만, 약간 복잡하여 그만한 가치가 없어 보였습니다.
즉, 개발자는 사실상 다음의 절충안 중 하나를 결정해야 합니다.
- 어셈블리 언로드 시 안전성: 할당이 필요하므로
delegate
이미 충분한 옵션입니다. - 조립 해제 시 안전성이 없으니
delegate*
을 사용하세요. 사용 목적에 맞게unsafe
컨텍스트 외부에서 쓸 수 있도록,struct
으로 감싸서 코드의 나머지 부분에 활용할 수 있습니다.
C# feature specifications