Wskaźniki funkcji
Notatka
Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.
Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Różnice te są ujęte w notatkach z odpowiednich spotkań projektowych języka (LDM) .
Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .
Streszczenie
Ta propozycja zawiera konstrukcje językowe, które uwidaczniają kode operacji IL, do których obecnie nie można uzyskać efektywnego dostępu lub w ogóle, w języku C#: ldftn
i calli
. Te kody operacji IL mogą być ważne w kodzie o wysokiej wydajności, a deweloperzy potrzebują wydajnego sposobu uzyskiwania do nich dostępu.
Motywacja
Motywacje i tło dla tej funkcji opisano w następującym zagadnieniu (tak jak potencjalna implementacja tej funkcji):
Jest to alternatywna propozycja projektu funkcji wewnętrznych kompilatora
Szczegółowy projekt
Wskaźniki funkcji
Język umożliwia deklarowanie wskaźników funkcji przy użyciu składni delegate*
. Pełną składnię opisano szczegółowo w następnej sekcji, ale ma ona przypominać składnię używaną przez Func
i deklaracje typów Action
.
unsafe class Example
{
void M(Action<int> a, delegate*<int, void> f)
{
a(42);
f(42);
}
}
Te typy są reprezentowane przy użyciu typu wskaźnika funkcji zgodnie z opisem w ECMA-335. Oznacza to, że wywołanie delegate*
będzie używać calli
, gdy wywołanie delegate
będzie używać callvirt
w metodzie Invoke
.
Choć składniowo wywołanie jest identyczne dla obu konstrukcji.
Definicja wskaźników metody ECMA-335 zawiera konwencję wywoływania w ramach podpisu typu (sekcja 7.1).
Domyślna konwencja wywoływania będzie managed
. Konwencje wywoływania niezarządzanego można określić, umieszczając słowo kluczowe unmanaged
po składni delegate*
, co spowoduje użycie domyślnej platformy systemu uruchomieniowego. Określone konwencje niezarządzane można następnie określić w nawiasach do słowa kluczowego unmanaged
, określając dowolny typ rozpoczynający się od CallConv
w przestrzeni nazw System.Runtime.CompilerServices
, pozostawiając prefiks CallConv
. Te typy muszą pochodzić z podstawowej biblioteki programu, a zestaw prawidłowych kombinacji jest zależny od platformy.
//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>;
Konwersje między typami delegate*
są wykonywane na podstawie ich podpisu, wraz z konwencją wywoływania.
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
}
}
Typ delegate*
jest typem wskaźnika, co oznacza, że ma wszystkie możliwości i ograniczenia standardowego typu wskaźnika:
- Jest prawidłowe tylko w kontekście
unsafe
. - Metody zawierające parametr
delegate*
lub typ zwracany mogą być wywoływane tylko z kontekstuunsafe
. - Nie można przekonwertować na
object
. - Nie można użyć jako argumentu ogólnego.
- Może niejawnie konwertować
delegate*
navoid*
. - Może jawnie konwertować z
void*
nadelegate*
.
Ograniczenia:
- Atrybutów niestandardowych nie można zastosować do
delegate*
ani żadnego z jego elementów. - Nie można oznaczyć parametru
delegate*
jakoparams
- Typ
delegate*
ma wszystkie ograniczenia normalnego typu wskaźnika. - Nie można wykonać arytmetyki wskaźnika bezpośrednio na typach wskaźników funkcji.
Składnia wskaźnika funkcji
Pełna składnia wskaźnika funkcji jest reprezentowana przez następującą gramatykę:
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'
;
Jeśli nie podano calling_convention_specifier
, wartość domyślna to managed
. Dokładne kodowanie metadanych calling_convention_specifier
oraz które identifier
są prawidłowe w unmanaged_calling_convention
, opisano w reprezentacji metadanych konwencji wywoływania .
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>>;
Konwersje wskaźnika funkcji
W niebezpiecznym kontekście zestaw dostępnych niejawnych konwersji jest rozszerzony, aby uwzględnić następujące niejawne konwersje wskaźnika:
- istniejące konwersje — (§23.5)
- Od funcptr_type
F0
do innego funcptr_typeF1
, pod warunkiem, że spełnione są wszystkie poniższe warunki:-
F0
iF1
mają taką samą liczbę parametrów, a każdy parametrD0n
wF0
ma ten samref
,out
lub modyfikatoryin
co odpowiedni parametrD1n
wF1
. - Dla każdego parametru wartości (czyli parametru bez modyfikatora
ref
,out
lubin
), istnieje konwersja tożsamości, niejawna konwersja odwołania lub niejawna konwersja wskaźnika z typu parametru wF0
do odpowiadającego typu parametru wF1
. - Dla każdego parametru
ref
,out
lubin
typ parametru wF0
jest taki sam jak odpowiedni typ parametru wF1
. - Jeśli zwracany typ jest według wartości (bez
ref
lubref readonly
), to istnieje tożsamość, niejawne odwołanie lub niejawna konwersja wskaźnika ze zwracanego typuF1
na zwracany typF0
. - Jeśli zwracany typ jest według odwołania (
ref
lubref readonly
), to zarówno zwracany typ, jak i modyfikatoryref
F1
są takie same jak zwracany typ i modyfikatoryref
F0
. - Konwencja wywoływania
F0
jest taka sama jak konwencja wywoływaniaF1
.
-
Pozwól na uzyskiwanie adresu metod
Grupy metod będą teraz dozwolone jako argumenty dla wyrażenia adres-of. Typ takiego wyrażenia to delegate*
, który ma równoważny podpis metody docelowej oraz zarządzaną konwencję wywoływania.
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;
}
}
W niebezpiecznym kontekście metoda M
jest zgodna z typem wskaźnika funkcji F
, jeśli spełnione są wszystkie następujące elementy:
-
M
iF
mają taką samą liczbę parametrów, a każdy parametr wM
ma ten samref
,out
lub modyfikatoryin
co odpowiedni parametr wF
. - Dla każdego parametru wartości (parametr bez
ref
,out
lub modyfikatorain
) istnieje konwersja identyczności, niejawna konwersja odwołania lub niejawna konwersja wskaźnika z typu parametru wM
na odpowiedni typ parametru wF
. - Dla każdego parametru
ref
,out
lubin
typ parametru wM
jest taki sam jak odpowiedni typ parametru wF
. - Jeśli zwracany typ jest według wartości (bez
ref
lubref readonly
), tożsamość, niejawne odwołanie lub niejawna konwersja wskaźnika istnieje z zwracanego typuF
do zwracanego typuM
. - Jeśli zwracany typ jest przekazywany przez odwołanie (
ref
lubref readonly
), to typ zwracany oraz modyfikatoryref
modułuF
są takie same jak typ zwracany i modyfikatoryref
modułuM
. - Konwencja wywoływania
M
jest taka sama jak konwencja wywoływaniaF
. Obejmuje to zarówno bit konwencji wywoływania, jak i wszystkie flagi konwencji wywoływania określone w identyfikatorze niezarządzanym. -
M
jest metodą statyczną.
W niebezpiecznym kontekście istnieje niejawna konwersja z wyrażenia adresu, którego celem jest grupa metod E
do odpowiedniego typu wskaźnika funkcji F
, jeśli E
zawiera co najmniej jedną metodę, którą można zastosować w postaci normalnej do listy argumentów skonstruowanej przy użyciu typów parametrów i modyfikatorów F
, jak opisano poniżej.
- Wybrano pojedynczą metodę
M
odpowiadającą wywołaniu metody formularzaE(A)
z następującymi modyfikacjami:- Lista argumentów
A
jest listą wyrażeń, z których każde jest klasyfikowane jako zmienna oraz ma typ i modyfikator (ref
,out
lubin
) odpowiadający elementom funcptr_parameter_list wF
. - Metody kandydatów to tylko te metody, które mają zastosowanie w ich normalnej postaci, a nie te, które mają zastosowanie w ich rozszerzonej formie.
- Metody kandydatów to tylko te metody, które są statyczne.
- Lista argumentów
- Jeśli algorytm rozwiązywania przeciążeń generuje błąd, wystąpi błąd czasu kompilacji. W przeciwnym razie algorytm tworzy jedną najlepszą metodę
M
mającą taką samą liczbę parametrów, jakF
, a konwersja jest uważana za istniejącą. - Wybrana metoda
M
musi być zgodna (zgodnie z definicją powyżej) z typem wskaźnika funkcjiF
. W przeciwnym razie wystąpi błąd czasu kompilacji. - Wynikiem konwersji jest wskaźnik funkcji typu
F
.
Oznacza to, że deweloperzy mogą polegać na regułach rozpoznawania przeciążenia, aby działały w połączeniu z operatorem adresu.
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;
}
}
Operator address-of zostanie zaimplementowany przy użyciu instrukcji ldftn
.
Ograniczenia tej funkcji:
- Dotyczy tylko metod oznaczonych jako
static
. - Nie można używać funkcji lokalnych innych niż
static
w&
. Szczegóły implementacji tych metod nie są celowo określone przez język. Obejmuje to, czy są statyczne, czy instancyjne, oraz dokładnie, z jaką sygnaturą są emitowane.
Operatory dla typów wskaźników funkcji
Sekcja w niebezpiecznym kodzie w wyrażeniach jest modyfikowana w następujący sposób:
W niebezpiecznym kontekście dostępnych jest kilka konstrukcji do pracy ze wszystkimi _pointer_type_s, które nie są _funcptr_type_s.
- Operator
*
może być używany do wykonywania inderekcji wskaźnika (§23.6.2).- Operator
->
może służyć do uzyskiwania dostępu do elementu członkowskiego struktury za pośrednictwem wskaźnika (§23.6.3).- Operator
[]
może służyć do indeksowania wskaźnika (§23.6.4).- Operator
&
może służyć do uzyskania adresu zmiennej (§23.6.5).- Operatory
++
i--
mogą służyć do przyrostowania i dekrementacji wskaźników (§23.6.6).- Operatory
+
i-
mogą służyć do wykonywania arytmetyki wskaźnika (§23.6.7).- Operatory
==
,!=
,<
,>
,<=
i=>
mogą służyć do porównywania wskaźników (§23.6.8).- Operator
stackalloc
może służyć do przydzielania pamięci ze stosu wywołań (§23.8).- Instrukcja
fixed
może służyć do tymczasowego naprawienia zmiennej, aby można było uzyskać jej adres (§23.7).W niebezpiecznym kontekście dostępnych jest kilka konstrukcji do działania na wszystkich _funcptr_type_s:
- Operator
&
może być używany do uzyskiwania adresu metod statycznych (Pozwól na uzyskiwanie adresu metod docelowych)- Operatory
==
,!=
,<
,>
,<=
i=>
mogą służyć do porównywania wskaźników (§23.6.8).
Ponadto zmodyfikujemy wszystkie sekcje w Pointers in expressions
, aby zabronić typów wskaźników funkcji, z wyjątkiem Pointer comparison
i The sizeof operator
.
Lepszy element członkowski funkcji
§12.6.4.3 Lepszy członek funkcji zostanie zmieniony, aby zawierał następujący wiersz:
delegate*
jest bardziej szczegółowa niżvoid*
Oznacza to, że można przeciążyć void*
i delegate*
i nadal rozsądnie używać operatora address-of.
Inferencja typów
W niebezpiecznym kodzie następujące zmiany są wprowadzane do algorytmów wnioskowania typów:
Typy danych wejściowych
Dodano następujące elementy:
Jeśli
E
jest grupą metod adresowych, aT
jest typem wskaźnika funkcji, wszystkie typy parametrówT
są typami wejściowymiE
z typemT
.
Typy danych wyjściowych
Dodano następujące elementy:
Jeśli
E
jest grupą metod adresową, aT
jest typem wskaźnika funkcji, zwracany typT
jest typem danych wyjściowychE
z typemT
.
Wnioskowanie typów danych wyjściowych
Następujący punktor jest dodawany między punktorami 2 i 3:
- Jeśli
E
jest grupą metod adresowych, aT
jest typem wskaźnika funkcji z typami parametrówT1...Tk
i zwracanym typemTb
, a rozpoznawanie przeciążeniaE
z typamiT1..Tk
daje jedną metodę z typem zwracanymU
U
, jest wykonywana zU
doTb
.
Lepsze przekształcenie z wyrażenia
Następujący punktor podrzędny jest dodawany jako przykład do punktora 2:
V
jest typem wskaźnika funkcjidelegate*<V2..Vk, V1>
, podczas gdyU
jest typem wskaźnika funkcjidelegate*<U2..Uk, U1>
. Konwencja wywoływaniaV
jest identyczna zU
, a referencjaVi
jest taka sama jakUi
.
Wnioskowanie o granicy dolnej
Następujący przypadek został dodany do punktu 3:
V
jest typem wskaźnika funkcjidelegate*<V2..Vk, V1>
i istnieje typ wskaźnika funkcjidelegate*<U2..Uk, U1>
taki, żeU
jest identyczny zdelegate*<U2..Uk, U1>
, a konwencja wywoływaniaV
jest identyczna zU
, a refnośćVi
jest identyczna zUi
.
Pierwszy punkt wnioskowania z Ui
do Vi
został zmodyfikowany na:
- Jeśli
U
nie jest typem wskaźnika funkcji iUi
nie jest znany jako typ referencyjny, lub jeśliU
jest typem wskaźnika funkcji, aUi
nie jest znany jako typ wskaźnika funkcji ani typ referencyjny, to zostaje wykonane dokładne wnioskowanie .
Następnie dodano po trzecim punkcie wnioskowania z Ui
do Vi
:
- W przeciwnym razie, jeśli
V
jestdelegate*<V2..Vk, V1>
, wnioskowanie zależy od i-tego parametrudelegate*<V2..Vk, V1>
:
- Jeśli wersja 1:
- Jeśli zwracana jest według wartości, zostanie wykonane wnioskowanie o niższej granicy.
- Jeśli zwracanie następuje przez odwołanie, zostanie wykonane dokładne wnioskowanie.
- Jeśli V2..Vk:
- Jeśli parametr jest według wartości, zostanie wykonane wnioskowanie górnej granicy.
- Jeśli parametr jest przy użyciu odwołania, zostanie wykonane dokładne wnioskowanie.
Wnioskowanie o górnej granicy
Następujący przypadek jest dodawany do punktu 2:
U
jest typem wskaźnika funkcjidelegate*<U2..Uk, U1>
, aV
jest typem wskaźnika funkcji, który jest identyczny zdelegate*<V2..Vk, V1>
, a konwencja wywoływaniaU
jest identyczna zV
, a refnośćUi
jest identyczna zVi
.
Pierwszy punkt wnioskowania z Ui
do Vi
został zmodyfikowany na:
- Jeśli
U
nie jest typem wskaźnika funkcji aniUi
nie jest znany jako typ odniesienia, lub jeśliU
jest typem wskaźnika funkcji, aUi
nie jest znany ani jako typ wskaźnika funkcji, ani jako typ odniesienia, zostanie wykonane dokładne wnioskowanie.
Następnie dodano po trzecim punkcie wnioskowania z Ui
do Vi
:
- W przeciwnym razie, jeśli
U
todelegate*<U2..Uk, U1>
, wnioskowanie zależy od i-tego parametrudelegate*<U2..Uk, U1>
:
- Jeśli U1:
- Jeśli zwracana jest według wartości, zostanie wykonana wnioskowanie górnej granicy.
- Jeśli zwrot odbywa się przez referencję, zostanie dokonane dokładne wnioskowanie .
- Jeśli U2..Uk:
- Jeśli parametr jest przekazywany przez wartość, zostanie wykonana inferencja dolnej granicy .
- Jeśli parametr jest przekazywany przez odniesienie, zostanie wykonane dokładne wnioskowanie.
Reprezentacja metadanych in
, out
i ref readonly
parametrów i typów zwracanych
Sygnatury wskaźników funkcji nie mają lokalizacji flag parametrów, dlatego musimy zakodować, czy parametry i typ zwracany są in
, out
lub ref readonly
za pomocą modreqs.
in
Ponownie użyjemy System.Runtime.InteropServices.InAttribute
, zastosowanego jako modreq
do specyfikatora 'ref' w parametrze lub typie zwracanym, aby oznaczać następujące kwestie:
- Jeśli zastosowano go do specyfikatora ref parametru, ten parametr jest traktowany jako
in
. - Jeśli zastosowano do specyfikatora ref typu zwracanego, zwracany typ jest traktowany jako
ref readonly
.
out
Używamy System.Runtime.InteropServices.OutAttribute
, które stosuje się jako modreq
do specyfikatora ref w typie parametru, aby wskazać, że parametr jest parametrem out
.
Błędy
- Stosowanie
OutAttribute
jako modreq dla typu zwracanego jest błędem. - Jest to błąd podczas stosowania zarówno
InAttribute
, jak iOutAttribute
jako modreq do typu parametru. - Jeśli dowolny z nich jest określony za pośrednictwem modopt, są one ignorowane.
Reprezentacja metadanych konwencji wywoływania
Konwencje wywoływania są kodowane w podpisie metody w metadanych przez kombinację flagi CallKind
w podpisie i zero lub więcej modopt
s na początku podpisu. EcMA-335 obecnie deklaruje następujące elementy w flagi CallKind
:
CallKind
: default
| unmanaged cdecl
| unmanaged fastcall
| unmanaged thiscall
| unmanaged stdcall
| varargs
;
Z tych elementów wskaźniki funkcji w języku C# będą obsługiwać wszystkie, z wyjątkiem varargs
.
Ponadto środowisko uruchomieniowe (oraz ostatecznie 335) zostanie zaktualizowane, aby uwzględniać nowy CallKind
na nowych platformach. Nie ma obecnie formalnej nazwy, ale ten dokument będzie używać unmanaged ext
jako symbol zastępczy do oznaczania nowego rozszerzalnego formatu konwencji wywoływania. Gdy brak modopt
s, unmanaged ext
jest domyślną konwencją wywoływania platformy, a unmanaged
występuje bez nawiasów kwadratowych.
Mapowanie calling_convention_specifier
na CallKind
calling_convention_specifier
, który zostanie pominięty lub określony jako managed
, odwzorowuje się na default
CallKind
. Jest to domyślny CallKind
dla jakiejkolwiek metody, która nie jest przypisana UnmanagedCallersOnly
.
Język C# rozpoznaje 4 specjalne identyfikatory mapowane na określone istniejące niezarządzane elementy zdefiniowane jako CallKind
w ECMA 335. Aby to mapowanie było wykonywane, te identyfikatory muszą być określone samodzielnie, bez innych identyfikatorów, a to wymaganie jest zakodowane w specyfikacji dla unmanaged_calling_convention
s. Te identyfikatory to Cdecl
, Thiscall
, Stdcall
i Fastcall
, które odpowiadają odpowiednio unmanaged cdecl
, unmanaged thiscall
, unmanaged stdcall
i unmanaged fastcall
. Jeśli określono więcej niż jedną identifer
lub pojedynczy identifier
nie należy do specjalnie uznawanych identyfikatorów, przeprowadzamy specjalne wyszukiwanie nazwy na identyfikatorze według następujących reguł:
- Poprzedzamy
identifier
ciągiemCallConv
- Przyjrzymy się tylko typom zdefiniowanym w przestrzeni nazw
System.Runtime.CompilerServices
. - Przyjrzymy się tylko typom zdefiniowanym w podstawowej bibliotece aplikacji, czyli tej, która definiuje
System.Object
i nie zależy od innych bibliotek. - Patrzymy tylko na typy publiczne.
Jeśli wyszukiwanie powiedzie się na wszystkich identifier
określonych w unmanaged_calling_convention
, kodujemy CallKind
jako unmanaged ext
i kodujemy każdy z rozpoznanych typów w zestawie modopt
s na początku podpisu wskaźnika funkcji. Należy zauważyć, że te reguły oznaczają, iż użytkownicy nie mogą poprzedzać tych identifier
tagami CallConv
, ponieważ spowoduje to wyszukanie CallConvCallConvVectorCall
.
Podczas interpretowania metadanych najpierw przyjrzymy się CallKind
. Jeśli jest to coś innego niż unmanaged ext
, ignorujemy wszystkie modopt
w typie zwrotnym dla określania konwencji wywoływania i wykorzystujemy tylko CallKind
. Jeśli CallKind
jest unmanaged ext
, przyjrzymy się modopts na początku typu wskaźnika funkcji, tworząc sumę wszystkich typów, które spełniają następujące wymagania:
- Element jest zdefiniowany w bibliotece podstawowej, czyli w bibliotece, która nie odwołuje się do innych bibliotek i definiuje
System.Object
. - Typ jest zdefiniowany w przestrzeni nazw
System.Runtime.CompilerServices
. - Typ rozpoczyna się od prefiksu
CallConv
. - Typ jest publiczny.
Reprezentują one typy, które należy znaleźć podczas wyszukiwania w identifier
s w unmanaged_calling_convention
podczas definiowania typu wskaźnika funkcji w źródle.
Jest to błąd podczas próby użycia wskaźnika funkcji z CallKind
unmanaged ext
, jeśli docelowe środowisko uruchomieniowe nie obsługuje tej funkcji. Zostanie to ustalone, wyszukując obecność stałej System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind
. Jeśli ta stała jest obecna, uznaje się, że środowisko uruchomieniowe obsługuje tę funkcję.
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
jest atrybutem używanym przez CLR, aby wskazać, że metoda powinna być wywoływana z określoną konwencją wywoływania. W związku z tym wprowadzamy następującą obsługę atrybutu:
- Bezpośrednie wywołanie metody oznaczonej tym atrybutem w języku C# jest błędem. Użytkownicy muszą uzyskać wskaźnik funkcji do metody, a następnie wywołać ten wskaźnik.
- Błędem jest stosowanie atrybutu do czegoś innego niż zwykła metoda statyczna lub zwykła lokalna funkcja statyczna. Kompilator języka C# oznaczy wszystkie niestatyczne lub statyczne nietypowe metody importowane z metadanych za pomocą tego atrybutu jako nieobsługiwane przez język.
- Jest to błąd, gdy metoda oznaczona atrybutem ma parametr lub zwracany typ, który nie jest
unmanaged_type
. - Jest to błędem, gdy metoda oznaczona atrybutem posiada parametry typu, jeśli te parametry typu są ograniczone do
unmanaged
. - Błędem jest oznaczenie metody w typie ogólnym przy pomocy atrybutu.
- Jest to błąd podczas konwertowania metody oznaczonej atrybutem na typ delegata.
- Popełniono błąd, podając jakiekolwiek typy dla
UnmanagedCallersOnly.CallConvs
, które nie spełniają wymagań dla konwencji wywoływaniamodopt
w metadanych.
Podczas określania konwencji wywoływania metody oznaczonej prawidłowym atrybutem UnmanagedCallersOnly
kompilator wykonuje następujące kontrole typów określonych we właściwości CallConvs
w celu określenia skutecznych CallKind
i modopt
, które powinny być używane do określania konwencji wywoływania:
- Jeśli nie określono żadnych typów,
CallKind
jest traktowana jakounmanaged ext
, bez konwencji wywoływaniamodopt
s na początku typu wskaźnika funkcji. - Jeśli określono jeden typ, a ten typ ma nazwę
CallConvCdecl
,CallConvThiscall
,CallConvStdcall
lubCallConvFastcall
,CallKind
jest traktowany jakounmanaged cdecl
,unmanaged thiscall
,unmanaged stdcall
lubunmanaged fastcall
, odpowiednio bez konwencji wywoływaniamodopt
s na początku typu wskaźnika funkcji. - Jeśli określono wiele typów lub pojedynczy typ nie jest nazwany jednym ze specjalnie wymienionych typów powyżej,
CallKind
jest traktowana jakounmanaged ext
, a unia określonych typów jest traktowana jakomodopt
na początku typu wskaźnika funkcji.
Następnie kompilator analizuje tę skuteczną kolekcję CallKind
i modopt
oraz używa normalnych zasad metadanych do określenia ostatecznej konwencji wywołania wskaźnika do funkcji.
Otwórz pytania
Wykrywanie obsługi środowiska uruchomieniowego dla unmanaged ext
https://github.com/dotnet/runtime/issues/38135 śledzi dodawanie tej flagi. W zależności od opinii z przeglądu użyjemy właściwości określonej w problemie lub użyjemy obecności UnmanagedCallersOnlyAttribute
jako flagi określającej, czy środowiska uruchomieniowe obsługują unmanaged ext
.
Zagadnienia dotyczące
Zezwalaj na metody wystąpień
Wniosek można rozszerzyć na obsługę metod instancji, korzystając z konwencji wywoływania CLI EXPLICITTHIS
(zwanej instance
w kodzie C#). Ten sposób wskaźników funkcji CLI umieszcza parametr this
jako jawny pierwszy parametr w składni wskaźnika funkcji.
unsafe class Instance {
void Use() {
delegate* instance<Instance, string> f = &ToString;
f(this);
}
}
To jest rozsądne, ale dodaje pewne komplikacje do propozycji. Szczególnie dlatego, że wskaźniki funkcji różniące się konwencją wywoływania instance
i managed
byłyby niezgodne, mimo że oba przypadki są używane do wywoływania metod zarządzanych z tym samym podpisem języka C#. We wszystkich rozważanych przypadkach, gdzie byłoby to wartościowe, istniało proste obejście: użycie lokalnej funkcji static
.
unsafe class Instance {
void Use() {
static string toString(Instance i) => i.ToString();
delegate*<Instance, string> f = &toString;
f(this);
}
}
Nie wymagaj niebezpiecznej deklaracji
Zamiast wymagać unsafe
przy każdym użyciu delegate*
, wymagaj jej tylko w punkcie, w którym grupa metod jest konwertowana na delegate*
. W tym miejscu pojawiają się podstawowe problemy z bezpieczeństwem (wiedząc, że nie można zwolnić zestawu zawierającego, gdy wartość jest aktywna). Wymaganie unsafe
w innych lokalizacjach może być postrzegane jako nadmierne.
W ten sposób projekt był pierwotnie zamierzony. Ale wynikające z tego reguły językowe wydawały się bardzo niezręczne. Nie można ukryć faktu, że jest to wartość wskaźnika i ciągle przebijała się nawet bez użycia słowa kluczowego unsafe
. Na przykład konwersja na object
nie może być dozwolona, nie może być członkiem class
itp. Projekt języka C# wymaga unsafe
dla wszystkich zastosowań wskaźnika, dlatego ten projekt jest zgodny z tym projektem.
Deweloperzy nadal będą w stanie przedstawić bezpieczne otoki na podstawie delegate*
wartości w taki sam sposób, jak w przypadku normalnych typów wskaźników dzisiaj. Rozważ
unsafe struct Action {
delegate*<void> _ptr;
Action(delegate*<void> ptr) => _ptr = ptr;
public void Invoke() => _ptr();
}
Korzystanie z delegatów
Zamiast używać nowego elementu składni, delegate*
, po prostu użyj istniejących typów delegate
z *
po typie:
Func<object, object, bool>* ptr = &object.ReferenceEquals;
Obsługa konwencji wywoływania może być wykonywana przez dodawanie adnotacji do typów delegate
za pomocą atrybutu określającego wartość CallingConvention
. Brak atrybutu wskazywałby na użycie konwencji wywoływania zarządzanego.
Kodowanie tego w IL jest problematyczne. Wartość bazowa musi być reprezentowana jako wskaźnik, ale musi również:
- Mają unikatowy typ, aby zezwolić na przeciążenia z różnymi typami wskaźnika funkcji.
- Być odpowiednikiem dla celów OHI w granicach modułów.
Ostatni punkt jest szczególnie problematyczny. Oznacza to, że każde zgromadzenie używające Func<int>*
musi kodować równoważny typ w metadanych, nawet jeśli Func<int>*
jest zdefiniowany w zgromadzeniu, którego nie kontroluje.
Ponadto każdy inny typ, który jest zdefiniowany z nazwą System.Func<T>
w zestawie, który nie jest mscorlib, musi być inny niż wersja zdefiniowana w mscorlib.
Jedną z zbadanych opcji było emitowanie takiego wskaźnika, jak mod_req(Func<int>) void*
. Nie działa to jednak, ponieważ mod_req
nie może się łączyć z TypeSpec
, w związku z tym, nie może odnosić się do ogólnych instancji.
Nazwane wskaźniki funkcji
Składnia wskaźnika funkcji może być kłopotliwa, zwłaszcza w skomplikowanych sytuacjach, takich jak zagnieżdżone wskaźniki funkcji. Zamiast kazać deweloperom za każdym razem wpisywać sygnaturę funkcji, język mógłby zezwalać na nazwane deklaracje wskaźników funkcji, podobnie jak w przykładzie delegate
.
func* void Action();
unsafe class NamedExample {
void M(Action a) {
a();
}
}
Część problemu polega na tym, że podstawowy prymityw interfejsu CLI nie ma nazw, dlatego byłoby to wyłącznie rozwiązanie dla języka C# i wymagałoby nieco pracy z metadanymi, aby to umożliwić. To jest możliwe, ale to znaczna ilość pracy. Zasadniczo język C# wymaga dodatkowego elementu, który wspiera tabelę definicji typów wyłącznie dla tych nazw.
Również w przypadku zbadania argumentów nazwanych wskaźników funkcji okazało się, że mogą one mieć równie dobre zastosowanie do wielu innych scenariuszy. Na przykład równie wygodne byłoby zadeklarowanie nazwanych krotek, aby zmniejszyć konieczność wpisywania pełnej sygnatury we wszystkich przypadkach.
(int x, int y) Point;
class NamedTupleExample {
void M(Point p) {
Console.WriteLine(p.x);
}
}
Po dyskusji postanowiliśmy nie zezwalać na nazwaną deklarację typów delegate*
. Jeśli okaże się, że na podstawie opinii klientów istnieje znacząca potrzeba, zbadamy rozwiązanie nazewnictwa, które będzie działać dla wskaźników funkcji, krotek, typów generycznych itp. Prawdopodobnie będzie to podobne do innych sugestii, takich jak pełna obsługa typedef
w języku.
Przyszłe zagadnienia
delegaty statyczne
Odnosi się to do propozycji umożliwiającej deklarowanie typów delegate
, które mogą odnosić się wyłącznie do członków static
. Zaletą jest to, że takie wystąpienia delegate
mogą być bez alokacji i lepsze w scenariuszach wrażliwych na wydajność.
Jeśli funkcja wskaźnika funkcji zostanie zaimplementowana, propozycja static delegate
prawdopodobnie zostanie zamknięta. Proponowaną zaletą tej funkcji jest swobodny charakter alokacji. Jednak ostatnie badania wykazały, że nie można tego osiągnąć z powodu rozładowania modułu. Musi istnieć silne powiązanie z static delegate
do metody, do której się odwołuje, aby zapobiec przedwczesnemu zwolnieniu zestawu.
Aby zachować każde wystąpienie static delegate
, konieczne byłoby przydzielenie nowego uchwytu, co jest sprzeczne z celami propozycji. Były pewne projekty, w których alokacja mogła być amortyzowana do pojedynczej alokacji na miejsce wywołania, ale to było nieco złożone i nie wydawało się warte tego poświęcenia.
Oznacza to, że deweloperzy muszą zasadniczo zdecydować między następującymi kompromisami:
- Bezpieczeństwo podczas rozładunku zespołu: wymaga to przydziałów, dlatego
delegate
jest już wystarczającą opcją. - Brak bezpieczeństwa przed rozładowaniem zestawu: użyj
delegate*
. Można to opakować wstruct
, aby zezwolić na użycie poza kontekstemunsafe
w pozostałej części kodu.
C# feature specifications