Udostępnij za pośrednictwem


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):

dotnet/csharplang#191

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 kontekstu unsafe.
  • Nie można przekonwertować na object.
  • Nie można użyć jako argumentu ogólnego.
  • Może niejawnie konwertować delegate* na void*.
  • Może jawnie konwertować z void* na delegate*.

Ograniczenia:

  • Atrybutów niestandardowych nie można zastosować do delegate* ani żadnego z jego elementów.
  • Nie można oznaczyć parametru delegate* jako params
  • 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 identifiersą 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_typeF0 do innego funcptr_typeF1, pod warunkiem, że spełnione są wszystkie poniższe warunki:
    • F0 i F1 mają taką samą liczbę parametrów, a każdy parametr D0n w F0 ma ten sam ref, outlub modyfikatory in co odpowiedni parametr D1n w F1.
    • Dla każdego parametru wartości (czyli parametru bez modyfikatora ref, outlub in), istnieje konwersja tożsamości, niejawna konwersja odwołania lub niejawna konwersja wskaźnika z typu parametru w F0 do odpowiadającego typu parametru w F1.
    • Dla każdego parametru ref, outlub in typ parametru w F0 jest taki sam jak odpowiedni typ parametru w F1.
    • Jeśli zwracany typ jest według wartości (bez ref lub ref readonly), to istnieje tożsamość, niejawne odwołanie lub niejawna konwersja wskaźnika ze zwracanego typu F1 na zwracany typ F0.
    • Jeśli zwracany typ jest według odwołania (ref lub ref readonly), to zarówno zwracany typ, jak i modyfikatory refF1 są takie same jak zwracany typ i modyfikatory refF0.
    • Konwencja wywoływania F0 jest taka sama jak konwencja wywoływania F1.

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 i F mają taką samą liczbę parametrów, a każdy parametr w M ma ten sam ref, outlub modyfikatory in co odpowiedni parametr w F.
  • Dla każdego parametru wartości (parametr bez ref, outlub modyfikatora in) istnieje konwersja identyczności, niejawna konwersja odwołania lub niejawna konwersja wskaźnika z typu parametru w M na odpowiedni typ parametru w F.
  • Dla każdego parametru ref, outlub in typ parametru w M jest taki sam jak odpowiedni typ parametru w F.
  • Jeśli zwracany typ jest według wartości (bez ref lub ref readonly), tożsamość, niejawne odwołanie lub niejawna konwersja wskaźnika istnieje z zwracanego typu F do zwracanego typu M.
  • Jeśli zwracany typ jest przekazywany przez odwołanie (ref lub ref readonly), to typ zwracany oraz modyfikatory ref modułu F są takie same jak typ zwracany i modyfikatory ref modułu M.
  • Konwencja wywoływania M jest taka sama jak konwencja wywoływania F. 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 formularza E(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, outlub in) odpowiadający elementom funcptr_parameter_list w F.
    • 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.
  • 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, jak F, a konwersja jest uważana za istniejącą.
  • Wybrana metoda M musi być zgodna (zgodnie z definicją powyżej) z typem wskaźnika funkcji F. 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:

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

§12.6.3.4

Dodano następujące elementy:

Jeśli E jest grupą metod adresowych, a T jest typem wskaźnika funkcji, wszystkie typy parametrów T są typami wejściowymi E z typem T.

Typy danych wyjściowych

§12.6.3.5

Dodano następujące elementy:

Jeśli E jest grupą metod adresową, a T jest typem wskaźnika funkcji, zwracany typ T jest typem danych wyjściowych E z typem T.

Wnioskowanie typów danych wyjściowych

§12.6.3.7

Następujący punktor jest dodawany między punktorami 2 i 3:

  • Jeśli E jest grupą metod adresowych, a T jest typem wskaźnika funkcji z typami parametrów T1...Tk i zwracanym typem Tb, a rozpoznawanie przeciążenia E z typami T1..Tk daje jedną metodę z typem zwracanym UU, jest wykonywana z U do Tb.

Lepsze przekształcenie z wyrażenia

§12.6.4.5

Następujący punktor podrzędny jest dodawany jako przykład do punktora 2:

  • V jest typem wskaźnika funkcji delegate*<V2..Vk, V1>, podczas gdy U jest typem wskaźnika funkcji delegate*<U2..Uk, U1>. Konwencja wywoływania V jest identyczna z U, a referencja Vi jest taka sama jak Ui.

Wnioskowanie o granicy dolnej

§12.6.3.10

Następujący przypadek został dodany do punktu 3:

  • V jest typem wskaźnika funkcji delegate*<V2..Vk, V1> i istnieje typ wskaźnika funkcji delegate*<U2..Uk, U1> taki, że U jest identyczny z delegate*<U2..Uk, U1>, a konwencja wywoływania V jest identyczna z U, a refność Vi jest identyczna z Ui.

Pierwszy punkt wnioskowania z Ui do Vi został zmodyfikowany na:

  • Jeśli U nie jest typem wskaźnika funkcji i Ui nie jest znany jako typ referencyjny, lub jeśli U jest typem wskaźnika funkcji, a Ui 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 jest delegate*<V2..Vk, V1>, wnioskowanie zależy od i-tego parametru delegate*<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

§12.6.3.11

Następujący przypadek jest dodawany do punktu 2:

  • U jest typem wskaźnika funkcji delegate*<U2..Uk, U1>, a V jest typem wskaźnika funkcji, który jest identyczny z delegate*<V2..Vk, V1>, a konwencja wywoływania U jest identyczna z V, a refność Ui jest identyczna z Vi.

Pierwszy punkt wnioskowania z Ui do Vi został zmodyfikowany na:

  • Jeśli U nie jest typem wskaźnika funkcji ani Ui nie jest znany jako typ odniesienia, lub jeśli U jest typem wskaźnika funkcji, a Ui 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 to delegate*<U2..Uk, U1>, wnioskowanie zależy od i-tego parametru delegate*<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, outi 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, outlub 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 i OutAttribute 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 modopts 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 modopts, 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 defaultCallKind. 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 CallKindw 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_conventions. Te identyfikatory to Cdecl, Thiscall, Stdcalli Fastcall, które odpowiadają odpowiednio unmanaged cdecl, unmanaged thiscall, unmanaged stdcalli 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ągiem CallConv
  • 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 identifierokreślonych w unmanaged_calling_convention, kodujemy CallKind jako unmanaged exti kodujemy każdy z rozpoznanych typów w zestawie modopts na początku podpisu wskaźnika funkcji. Należy zauważyć, że te reguły oznaczają, iż użytkownicy nie mogą poprzedzać tych identifiertagami CallConv, ponieważ spowoduje to wyszukanie CallConvCallConvVectorCall.

Podczas interpretowania metadanych najpierw przyjrzymy się CallKind. Jeśli jest to coś innego niż unmanaged ext, ignorujemy wszystkie modoptw 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 identifiers 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 CallKindunmanaged 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ływania modoptw 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 jako unmanaged ext, bez konwencji wywoływania modopts na początku typu wskaźnika funkcji.
  • Jeśli określono jeden typ, a ten typ ma nazwę CallConvCdecl, CallConvThiscall, CallConvStdcalllub CallConvFastcall, CallKind jest traktowany jako unmanaged cdecl, unmanaged thiscall, unmanaged stdcalllub unmanaged fastcall, odpowiednio bez konwencji wywoływania modopts 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 jako unmanaged ext, a unia określonych typów jest traktowana jako modoptna 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 classitp. 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ż:

  1. Mają unikatowy typ, aby zezwolić na przeciążenia z różnymi typami wskaźnika funkcji.
  2. 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:

  1. Bezpieczeństwo podczas rozładunku zespołu: wymaga to przydziałów, dlatego delegate jest już wystarczającą opcją.
  2. Brak bezpieczeństwa przed rozładowaniem zestawu: użyj delegate*. Można to opakować w struct, aby zezwolić na użycie poza kontekstem unsafe w pozostałej części kodu.