Bagikan melalui


Penunjuk Fungsi

Nota

Artikel ini adalah spesifikasi fitur. Spesifikasi berfungsi sebagai dokumen desain untuk fitur tersebut. Ini termasuk perubahan spesifikasi yang diusulkan, bersama dengan informasi yang diperlukan selama desain dan pengembangan fitur. Artikel ini diterbitkan sampai perubahan spesifikasi yang diusulkan diselesaikan dan dimasukkan dalam spesifikasi ECMA saat ini.

Mungkin ada beberapa perbedaan antara spesifikasi fitur dan implementasi yang selesai. Perbedaan tersebut dicatat dalam catatan terkait rapat desain bahasa (LDM) .

Anda dapat mempelajari lebih lanjut tentang proses untuk mengadopsi speklet fitur ke dalam standar bahasa C# dalam artikel tentang spesifikasi .

Ringkasan

Proposal ini menyediakan konstruksi bahasa yang mengekspos opcode IL yang saat ini tidak dapat diakses secara efisien, atau sama sekali tidak bisa diakses, di C# saat ini: ldftn dan calli. Opcode IL ini dapat menjadi penting dalam kode performa tinggi dan pengembang membutuhkan cara yang efisien untuk mengaksesnya.

Motivasi

Motivasi dan latar belakang untuk fitur ini dijelaskan dalam isu berikut (termasuk potensi implementasi fitur):

dotnet/csharplang#191

Ini adalah proposal desain alternatif untuk intrinsik kompilator

Desain Terperinci

Penunjuk fungsi

Bahasa akan memungkinkan deklarasi penunjuk fungsi menggunakan sintaks delegate*. Sintaks lengkap dijelaskan secara rinci di bagian berikutnya tetapi dimaksudkan untuk menyerupai sintaks yang digunakan oleh deklarasi jenis Func dan Action.

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

Jenis ini diwakili menggunakan jenis penunjuk fungsi seperti yang diuraikan dalam ECMA-335. Ini berarti pemanggilan delegate* akan menggunakan calli di mana pemanggilan delegate akan menggunakan callvirt pada metode Invoke. Meskipun secara sintaksis pemanggilan identik untuk kedua konstruksi.

Definisi ECMA-335 dari penunjuk metode mencakup konvensi panggilan sebagai bagian dari tanda tangan jenis (bagian 7.1). Konvensi panggilan default akan managed. Konvensi pemanggilan yang tidak dikelola dapat ditentukan dengan menempatkan kata kunci unmanaged setelah sintaks delegate*, yang akan menggunakan default platform runtime. Konvensi tertentu yang tidak dikelola kemudian dapat ditentukan dalam tanda kurung ke kata kunci unmanaged dengan menentukan jenis apa pun yang dimulai dengan CallConv di namespace System.Runtime.CompilerServices, meninggalkan awalan CallConv. Jenis-jenis ini harus berasal dari pustaka inti program, dan serangkaian kombinasi yang valid bergantung pada platform.

//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>;

Konversi antara jenis delegate* dilakukan berdasarkan tanda tangannya termasuk konvensi panggilan.

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
    }
}

Jenis delegate* adalah jenis penunjuk yang berarti memiliki semua kemampuan dan batasan jenis pointer standar:

  • Hanya berlaku dalam konteks unsafe.
  • Metode yang berisi parameter delegate* atau jenis pengembalian hanya dapat dipanggil dari konteks unsafe.
  • Tidak dapat dikonversi ke object.
  • Tidak dapat digunakan sebagai argumen generik.
  • Dapat secara implisit mengonversi delegate* ke void*.
  • Dapat secara eksplisit mengonversi dari void* ke delegate*.

Restriksi:

  • Atribut kustom tidak dapat diterapkan ke delegate* atau elemennya.
  • Parameter delegate* tidak dapat ditandai sebagai params
  • Jenis delegate* memiliki semua batasan jenis penunjuk normal.
  • Aritmatika penunjuk tidak dapat dilakukan secara langsung pada jenis penunjuk fungsi.

Sintaks penunjuk fungsi

Sintaks penunjuk fungsi lengkap diwakili oleh tata bahasa berikut:

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'
    ;

Jika tidak ada calling_convention_specifier yang disediakan, defaultnya adalah managed. Pengodean metadata yang tepat dari calling_convention_specifier dan identifieryang valid dalam unmanaged_calling_convention dibahas dalam Representasi Metadata Konvensi Panggilan.

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>>;

Konversi penunjuk fungsi

Dalam konteks tidak aman, kumpulan konversi implisit yang tersedia (Konversi implisit) diperluas untuk menyertakan konversi pointer implisit berikut:

  • konversi yang ada - (§23,5)
  • Dari funcptr_typeF0 ke funcptr_typeF1lain , asalkan semua hal berikut ini benar:
    • F0 dan F1 memiliki jumlah parameter yang sama, dan setiap parameter D0n di F0 memiliki pengubah ref, out, atau in yang sama dengan parameter yang sesuai D1n di F1.
    • Untuk setiap parameter nilai (parameter tanpa pengubah ref, out, atau in), konversi identitas, konversi referensi implisit, atau konversi pointer implisit ada dari jenis parameter di F0 ke jenis parameter yang sesuai di F1.
    • Untuk setiap parameter ref, out, atau in, jenis parameter dalam F0 sama dengan jenis parameter yang sesuai di F1.
    • Jika jenis pengembalian berdasarkan nilai (tidak ada ref atau ref readonly), identitas, referensi implisit, atau konversi pointer implisit ada dari jenis pengembalian F1 ke jenis pengembalian F0.
    • Jika jenis pengembalian adalah berdasarkan referensi (ref atau ref readonly), maka jenis pengembalian dan pengubah ref untuk F1 sama dengan jenis pengembalian dan pengubah ref untuk F0.
    • Konvensi panggilan F0 sama dengan konvensi panggilan F1.

Izinkan referensi ke metode tujuan

Grup metode sekarang akan diizinkan sebagai argumen ke alamat ekspresi. Jenis ekspresi seperti itu akan menjadi delegate* yang memiliki tanda tangan yang setara dari metode target dan konvensi panggilan terkelola:

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;
   }
}

Dalam konteks tidak aman, metode M kompatibel dengan jenis penunjuk fungsi F jika semua hal berikut ini benar:

  • M dan F memiliki jumlah parameter yang sama, dan setiap parameter dalam M memiliki pengubah ref, out, atau in yang sama dengan parameter yang sesuai di F.
  • Untuk setiap parameter nilai (parameter tanpa pengubah ref, out, atau in), konversi identitas, konversi referensi implisit, atau konversi pointer implisit ada dari jenis parameter di M ke jenis parameter yang sesuai di F.
  • Untuk setiap parameter ref, out, atau in, jenis parameter dalam M sama dengan jenis parameter yang sesuai di F.
  • Jika jenis pengembalian berdasarkan nilai (tidak ada ref atau ref readonly), identitas, referensi implisit, atau konversi pointer implisit ada dari jenis pengembalian F ke jenis pengembalian M.
  • Jika jenis pengembalian adalah berdasarkan referensi (ref atau ref readonly), maka jenis pengembalian dan pengubah ref untuk F sama dengan jenis pengembalian dan pengubah ref untuk M.
  • Konvensi panggilan M sama dengan konvensi panggilan F. Ini termasuk bit konvensi panggilan, serta bendera konvensi panggilan apa pun yang ditentukan dalam pengidentifikasi yang tidak dikelola.
  • M adalah metode statis.

Dalam konteks tidak aman, konversi implisit ada dari alamat ekspresi yang targetnya adalah grup metode E ke jenis penunjuk fungsi yang kompatibel F jika E berisi setidaknya satu metode yang berlaku dalam bentuk normalnya ke daftar argumen yang dibangun dengan menggunakan jenis parameter dan pengubah F, seperti yang dijelaskan dalam hal berikut.

  • Metode tunggal M dipilih sesuai dengan pemanggilan metode dalam bentuk E(A) dengan modifikasi berikut:
    • Daftar argumen A adalah daftar ekspresi, yang masing-masing diklasifikasikan sebagai variabel dengan jenis dan pengubah (ref, out, atau in) yang sesuai dengan funcptr_parameter_list dari F.
    • Metode kandidat hanyalah metode yang berlaku dalam bentuk normal mereka, bukan yang berlaku dalam bentuk yang diperluas.
    • Metode kandidat adalah hanya metode yang bersifat statis.
  • Jika algoritma resolusi kelebihan beban menghasilkan kesalahan, maka terjadi kesalahan waktu kompilasi. Jika tidak, algoritma menghasilkan satu metode terbaik M memiliki jumlah parameter yang sama dengan F dan konversi dianggap ada.
  • Metode yang dipilih M harus kompatibel (seperti yang didefinisikan di atas) dengan jenis penunjuk fungsi F. Jika tidak, terjadi kesalahan waktu kompilasi.
  • Hasil konversi adalah penunjuk fungsi jenis F.

Ini berarti pengembang dapat bergantung pada aturan resolusi kelebihan beban untuk bekerja bersama dengan alamat operator:

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;
    }
}

Alamat operator akan diimplementasikan menggunakan instruksi ldftn.

Pembatasan fitur ini:

  • Hanya berlaku untuk metode yang ditandai sebagai static.
  • Fungsi lokal non-static tidak dapat digunakan dalam &. Detail implementasi metode ini sengaja tidak ditentukan oleh bahasa. Ini termasuk apakah mereka statis atau instans serta sintaks persis apa yang mereka gunakan.

Operator pada Jenis Penunjuk Fungsi

Bagian dalam kode tidak aman pada ekspresi dimodifikasi seperti:

Dalam konteks yang tidak aman, beberapa konstruk tersedia untuk digunakan pada semua _pointer_type_s yang bukan _funcptr_type_s:

  • Operator * dapat digunakan untuk melakukan pengindereksian pointer (§23.6.2).
  • Operator -> dapat digunakan untuk mengakses anggota struct melalui pointer (§23.6.3).
  • Operator [] dapat digunakan untuk mengindeks pointer (§23,6,4).
  • Operator & dapat digunakan untuk mendapatkan alamat variabel (§23.6.5).
  • Operator ++ dan -- dapat digunakan untuk kenaikan dan penunjuk penurunan (§23,6,6).
  • Operator + dan - dapat digunakan untuk melakukan aritmatika pointer (§23,6,7).
  • Operator ==, !=, <, >, <=, dan => dapat digunakan untuk membandingkan pointer (§23,6,8).
  • Operator stackalloc dapat digunakan untuk mengalokasikan memori dari tumpukan panggilan (§23,8).
  • Pernyataan fixed dapat digunakan untuk memperbaiki variabel untuk sementara waktu sehingga alamatnya dapat diperoleh (§23,7).

Dalam konteks yang tidak aman, beberapa struktur tersedia untuk beroperasi pada semua _funcptr_type_s.

Selain itu, kami memodifikasi semua bagian dalam Pointers in expressions untuk melarang jenis penunjuk fungsi, kecuali Pointer comparison dan The sizeof operator.

Anggota fungsi yang lebih baik

§12.6.4.3 Anggota fungsi yang lebih baik akan diubah untuk menyertakan baris berikut:

delegate* lebih spesifik daripada void*

Ini berarti bahwa dimungkinkan untuk kelebihan beban pada void* dan delegate* dan masih sensib menggunakan alamat operator.

Inferensi Jenis

Dalam kode yang tidak aman, perubahan berikut dilakukan pada algoritma inferensi jenis:

Jenis input

§12.6.3.4

Berikut ini ditambahkan:

Jika E adalah grup metode alamat dan T adalah jenis penunjuk fungsi, maka semua jenis parameter T adalah jenis input E dengan jenis T.

Jenis output

§12.6.3.5

Berikut ini ditambahkan:

Jika E adalah grup metode pengalamatan dan T adalah jenis penunjuk fungsi, maka jenis pengembalian T adalah jenis keluaran dari E dengan jenis T.

Inferensi jenis output

§12.6.3.7

Butir berikut ditambahkan di antara butir 2 dan 3:

  • Jika adalah grup metode alamat dan adalah jenis penunjuk fungsi dengan jenis parameter dan jenis pengembalian , dan resolusi kelebihan beban dengan jenis menghasilkan satu metode dengan jenis pengembalian , maka inferensi terikat lebih rendah dibuat dari ke .

Konversi yang lebih baik dari ekspresi

§12.6.4.5

Sub-poin berikut ditambahkan ke dalam daftar poin 2:

  • V adalah tipe pointer fungsi delegate*<V2..Vk, V1> dan U adalah tipe pointer fungsi delegate*<U2..Uk, U1>, dan konvensi pemanggilan V identik dengan U, serta refness Vi identik dengan Ui.

Inferensi batas bawah

§12.6.3.10

Kasus berikut ditambahkan ke poin 3:

  • V adalah jenis penunjuk fungsi delegate*<V2..Vk, V1> dan ada jenis penunjuk fungsi delegate*<U2..Uk, U1> sedemikian rupa sehingga U identik dengan delegate*<U2..Uk, U1>, dan konvensi panggilan V identik dengan U, dan sifat referensi Vi identik dengan Ui.

Poin inferensi pertama dari Ui ke Vi dimodifikasi menjadi:

  • Jika U bukan tipe penunjuk fungsi dan Ui tidak diketahui sebagai jenis referensi, atau jika U adalah jenis penunjuk fungsi dan Ui tidak diketahui sebagai jenis penunjuk fungsi atau jenis referensi, maka inferensi yang tepat dibuat

Kemudian, ditambahkan setelah poin inferensi ke-3 dari Ui ke Vi:

  • Jika tidak, jika V adalah delegate*<V2..Vk, V1> maka inferensi tergantung pada parameter ke-i dari delegate*<V2..Vk, V1>:
    • Jika V1:
      • Jika pengembalian berdasarkan nilai, maka inferensi batas bawah dibuat.
      • Jika pengembaliannya adalah dengan referensi, maka inferensi persis dibuat.
    • Jika V2..Vk:
      • Jika parameter berdasarkan nilai, maka suatu inferensi terikat atas dibuat.
      • Jika parameter berdasarkan referensi, maka inferensi yang tepat dibuat.

Inferensi terikat atas

§12.6.3.11

Kasus berikut ditambahkan ke poin 2:

  • U adalah jenis penunjuk fungsi delegate*<U2..Uk, U1> dan V adalah jenis penunjuk fungsi yang identik dengan delegate*<V2..Vk, V1>, dan konvensi panggilan U identik dengan V, dan sifat referensi Ui identik dengan Vi.

Poin inferensi pertama dari Ui ke Vi dimodifikasi menjadi:

  • Jika U bukan tipe penunjuk fungsi dan Ui tidak diketahui sebagai jenis referensi, atau jika U adalah jenis penunjuk fungsi dan Ui tidak diketahui sebagai jenis penunjuk fungsi atau jenis referensi, maka inferensi yang tepat dibuat

Kemudian ditambahkan setelah poin inferensi ke-3 dari Ui ke Vi:

  • Jika tidak, jika U adalah delegate*<U2..Uk, U1> maka inferensi tergantung pada parameter ke-i dari delegate*<U2..Uk, U1>:
    • Jika U1:
      • Jika pengembalian berdasarkan nilai, maka inferensi batas atas dibuat.
      • Jika pengembaliannya adalah dengan referensi, maka inferensi persis dibuat.
    • Jika U2..Uk:
      • Jika parameter berdasarkan nilai, maka dibuat inferensi batas bawah .
      • Jika parameter berdasarkan referensi, maka inferensi yang tepat dibuat.

Representasi metadata parameter in, out, dan ref readonly dan jenis pengembalian

Signatur pointer fungsi tidak memiliki letak bendera parameter, jadi kita harus mengodekan apakah parameter dan tipe kembalian adalah in, out, atau ref readonly dengan menggunakan modreqs.

in

Kita menggunakan ulang System.Runtime.InteropServices.InAttribute, diterapkan sebagai modreq ke penentu referensi pada parameter atau jenis pengembalian, yang berarti sebagai berikut:

  • Jika diterapkan ke penentu ref parameter, parameter ini diperlakukan sebagai in.
  • Jika diterapkan ke penentu ref jenis pengembalian, jenis pengembalian diperlakukan sebagai ref readonly.

out

Kami menggunakan System.Runtime.InteropServices.OutAttribute, diterapkan sebagai modreq pada penentu ref dari jenis parameter, artinya parameter tersebut adalah parameter out.

Kesalahan

  • Ini adalah kesalahan untuk menerapkan OutAttribute sebagai modreq ke tipe pengembalian.
  • Ini adalah kesalahan untuk menerapkan secara bersamaan InAttribute dan OutAttribute sebagai modreq ke jenis parameter.
  • Jika salah satu ditentukan melalui modopt, maka akan diabaikan.

Representasi Metadata dari Konvensi Panggilan

Konvensi panggilan dikodekan dalam tanda tangan metode dalam metadata dengan kombinasi bendera CallKind dalam tanda tangan dan nol atau lebih modoptdi awal tanda tangan. ECMA-335 saat ini mendeklarasikan elemen berikut dalam bendera CallKind:

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

Dari jumlah tersebut, penunjuk fungsi di C# akan mendukung semua kecuali varargs.

Selain itu, runtime (dan nantinya versi 335) akan diperbarui untuk memasukkan CallKind baru pada platform baru. Saat ini belum memiliki nama formal, namun dokumen ini akan menggunakan unmanaged ext sebagai pengganti untuk format konvensi pemanggilan yang dapat diperluas. Tanpa modopt, unmanaged ext adalah konvensi panggilan default platform, unmanaged tanpa tanda kurung siku.

Memetakan calling_convention_specifier ke CallKind

calling_convention_specifier yang dihilangkan, atau ditentukan sebagai managed, dipetakan kedefaultCallKind. Ini adalah CallKind default dari metode apa pun yang tidak dikaitkan dengan UnmanagedCallersOnly.

C# mengenali 4 pengidentifikasi spesial yang memetakan ke komponen CallKindtak terkelola tertentu yang ada dari ECMA 335. Agar pemetaan ini terjadi, pengidentifikasi ini harus ditentukan sendiri, tanpa pengidentifikasi lain, dan persyaratan ini dikodekan ke dalam spesifikasi untuk unmanaged_calling_conventions. Pengidentifikasi ini adalah Cdecl, Thiscall, Stdcall, dan Fastcall, yang masing-masing sesuai dengan unmanaged cdecl, unmanaged thiscall, unmanaged stdcall, dan unmanaged fastcall. Jika lebih dari satu identifer ditentukan, atau satu identifier bukan dari pengidentifikasi yang dikenali secara khusus, kami melakukan pencarian nama khusus pada pengidentifikasi dengan aturan berikut:

  • Kami menambahkan identifier dengan string CallConv
  • Kita hanya melihat jenis yang ditentukan dalam namespace System.Runtime.CompilerServices.
  • Kita hanya melihat jenis yang ditentukan dalam pustaka inti aplikasi, yang merupakan pustaka yang mendefinisikan System.Object dan tidak memiliki dependensi.
  • Kami hanya memperhatikan tipe publik.

Jika pencarian berhasil di semua identifieryang ditentukan dalam unmanaged_calling_convention, kami mengodekan CallKind sebagai unmanaged ext, dan mengodekan setiap tipe yang telah diselesaikan dalam kumpulan modoptdi awal tanda tangan penunjuk fungsi. Sebagai catatan, aturan ini berarti bahwa pengguna tidak dapat menambahkan awalan identifierini dengan CallConv, karena itu akan menyebabkan pencarian CallConvCallConvVectorCall.

Saat menafsirkan metadata, pertama-tama kita melihat CallKind. Jika itu adalah apa pun selain unmanaged ext, kami mengabaikan semua modoptpada jenis pengembalian untuk tujuan menentukan konvensi panggilan, dan hanya menggunakan CallKind. Jika CallKind adalah unmanaged ext, kita melihat pada modopts di awal dari jenis penunjuk fungsi, dengan mengambil penyatuan semua jenis yang memenuhi persyaratan berikut:

  • ditentukan dalam pustaka inti, yang merupakan pustaka yang tidak mereferensikan pustaka lain dan mendefinisikan System.Object.
  • Jenis didefinisikan dalam namespace System.Runtime.CompilerServices.
  • Jenis dimulai dengan awalan CallConv.
  • Jenisnya adalah publik.

Ini mewakili jenis yang harus ditemukan saat melakukan pencarian pada identifierdalam unmanaged_calling_convention saat menentukan jenis penunjuk fungsi di sumber.

Ini adalah kesalahan untuk mencoba menggunakan penunjuk fungsi dengan CallKindunmanaged ext jika runtime target tidak mendukung fitur tersebut. Ini akan ditentukan dengan mencari keberadaan konstanta System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind. Jika konstanta ini ada, runtime dianggap mendukung fitur tersebut.

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute adalah atribut yang digunakan oleh CLR untuk menunjukkan bahwa metode harus dipanggil dengan konvensi panggilan tertentu. Karena itu, kami memperkenalkan dukungan berikut untuk bekerja dengan atribut :

  • Ini adalah kesalahan untuk langsung memanggil metode yang dianotasi dengan atribut ini dari C#. Pengguna harus mendapatkan penunjuk fungsi ke metode lalu memanggil penunjuk tersebut.
  • Ini adalah kesalahan untuk menerapkan atribut ke apa pun selain metode statis biasa atau fungsi lokal statis biasa. Pengkompilasi C# akan menandai metode non-statis atau statis non-biasa yang diimpor dari metadata dengan atribut ini sebagai tidak didukung oleh bahasa.
  • Ini adalah kesalahan bagi metode yang ditandai dengan atribut memiliki parameter atau tipe pengembalian yang bukan unmanaged_type.
  • Ini adalah kesalahan jika metode yang ditandai dengan atribut memiliki parameter jenis, bahkan jika parameter jenis tersebut dibatasi oleh unmanaged.
  • Merupakan kesalahan jika sebuah metode dalam jenis generik ditandai dengan atribut.
  • Ini adalah kesalahan untuk mengonversi metode yang ditandai dengan atribut ke tipe delegasi.
  • Ini adalah kesalahan untuk menentukan tipe apa pun untuk UnmanagedCallersOnly.CallConvs yang tidak sesuai dengan persyaratan konvensi pemanggilan modoptdalam metadata.

Saat menentukan konvensi panggilan metode yang ditandai dengan atribut UnmanagedCallersOnly yang valid, pengkompilasi melakukan pemeriksaan berikut pada jenis yang ditentukan dalam properti CallConvs untuk menentukan CallKind dan modoptyang efektif yang harus digunakan untuk menentukan konvensi panggilan:

  • Jika tidak ada jenis yang ditentukan, CallKind diperlakukan sebagai unmanaged ext, tanpa konvensi panggilan modoptdi awal jenis penunjuk fungsi.
  • Jika ada satu jenis yang ditentukan, dan jenis tersebut diberi nama CallConvCdecl, CallConvThiscall, CallConvStdcall, atau CallConvFastcall, CallKind diperlakukan sebagai unmanaged cdecl, unmanaged thiscall, unmanaged stdcall, atau unmanaged fastcall, masing-masing, tanpa konvensi panggilan modoptdi awal jenis penunjuk fungsi.
  • Jika beberapa jenis ditentukan atau jenis tunggal tidak dinamai salah satu jenis yang dipanggil secara khusus di atas, CallKind diperlakukan sebagai unmanaged ext, dengan gabungan jenis yang ditentukan diperlakukan sebagai modoptpada awal jenis penunjuk fungsi.

Pengkompilasi kemudian melihat pengumpulan CallKind dan modopt yang efektif ini dan menggunakan aturan metadata normal untuk menentukan konvensi panggilan akhir dari jenis penunjuk fungsi.

Buka Pertanyaan

Mendeteksi dukungan runtime untuk unmanaged ext

https://github.com/dotnet/runtime/issues/38135 melacak penambahan flag ini. Bergantung pada umpan balik tinjauan, kami akan menggunakan properti yang ditentukan dalam isu, atau menggunakan kehadiran UnmanagedCallersOnlyAttribute sebagai tanda yang menentukan apakah runtime mendukung unmanaged ext.

Pertimbangan

Mengizinkan metode instans

Proposal dapat diperluas untuk mendukung metode instans dengan memanfaatkan konvensi panggilan CLI EXPLICITTHIS (bernama instance dalam kode C#). Bentuk penunjuk fungsi CLI ini menempatkan parameter this sebagai parameter pertama eksplisit dari sintaks penunjuk fungsi.

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

Ini masuk akal tetapi menambahkan beberapa komplikasi pada proposal. Terutama karena penunjuk fungsi yang berbeda dengan konvensi panggilan instance dan managed akan tidak kompatibel meskipun kedua kasus digunakan untuk memanggil metode terkelola dengan tanda tangan C# yang sama. Selain itu, dalam setiap kasus yang dipertimbangkan di mana penting untuk memiliki ini, ada solusi sederhana: gunakan fungsi lokal static.

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

Tidak memerlukan pengaturan 'unsafe' saat deklarasi

Alih-alih mengharuskan unsafe pada setiap penggunaan delegate*, hanya memerlukannya pada titik di mana grup metode dikonversi ke delegate*. Di sinilah masalah keamanan inti mulai muncul (mengetahui bahwa rakitan yang mengandung tidak dapat dibongkar saat nilainya masih aktif). Membutuhkan unsafe di lokasi-lokasi lainnya dapat dilihat sebagai berlebihan.

Ini adalah bagaimana desain awalnya dimaksudkan. Tetapi aturan bahasa yang dihasilkan terasa sangat canggung. Tidak mungkin untuk menyembunyikan fakta bahwa ini adalah nilai pointer dan tetap muncul bahkan tanpa kata kunci unsafe. Misalnya konversi ke object tidak dapat diizinkan, itu tidak dapat menjadi anggota class, dll ... Desain C# memerlukan unsafe untuk semua penggunaan pointer dan karenanya desain ini mengikutinya.

Pengembang masih akan mampu menyajikan pembungkus aman di atas nilai dengan cara yang sama seperti yang mereka lakukan untuk jenis pointer normal saat ini. Anggap:

unsafe struct Action {
    delegate*<void> _ptr;

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

Menggunakan delegasi

Alih-alih menggunakan elemen sintaks baru, delegate*, cukup gunakan jenis delegate yang sudah ada dengan * yang mengikuti jenis tersebut.

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

Penanganan konvensi panggilan dapat dilakukan dengan menambahkan anotasi pada jenis delegate dengan atribut yang menentukan nilai CallingConvention. Kurangnya atribut akan menandakan konvensi panggilan terkelola.

Pengkodean di IL bermasalah. Nilai yang mendasar perlu diwakili sebagai penunjuk namun juga harus:

  1. Memiliki jenis unik untuk memungkinkan kelebihan beban dengan jenis penunjuk fungsi yang berbeda.
  2. Memenuhi persyaratan untuk tujuan OHI di seluruh batas perakitan.

Poin terakhir sangat bermasalah. Ini berarti bahwa setiap rakitan yang menggunakan Func<int>* harus mengodekan jenis yang setara dalam metadata walaupun Func<int>* didefinisikan dalam rakitan yang tidak dikendalikannya. Selain itu jenis lain yang didefinisikan dengan nama System.Func<T> dalam rakitan yang bukan mscorlib harus berbeda dari versi yang ditentukan dalam mscorlib.

Salah satu opsi yang dijelajahi adalah memancarkan penunjuk seperti mod_req(Func<int>) void*. Ini tidak berfungsi karena mod_req tidak dapat mengikat ke TypeSpec dan maka tidak dapat menargetkan instansiasi generik.

Penunjuk fungsi bernama

Sintaks penunjuk fungsi dapat rumit, terutama dalam kasus kompleks seperti penunjuk fungsi berlapis. Daripada membuat pengembang mengetik tanda tangan fungsi setiap kali, bahasa dapat mendukung deklarasi penunjuk fungsi yang dinamai, seperti yang dilakukan dengan delegate.

func* void Action();

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

Bagian dari masalah di sini adalah primitif CLI yang mendasar tidak memiliki nama sehingga ini akan murni penemuan C# dan memerlukan sedikit pekerjaan metadata untuk diaktifkan. Itu dapat dilakukan tetapi memerlukan banyak pekerjaan. Ini pada dasarnya mengharuskan C# untuk memiliki pendamping untuk tabel definisi tipe murni untuk nama-nama ini.

Selain itu, ketika argumen untuk penunjuk fungsi bernama diperiksa, kami menemukan bahwa argumen tersebut dapat diterapkan dengan sama baiknya untuk sejumlah skenario lain. Misalnya, akan sama praktisnya untuk mendeklarasikan tuple bernama demi mengurangi kebutuhan mengetikkan tanda lengkap dalam semua kasus.

(int x, int y) Point;

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

Setelah diskusi, kami memutuskan untuk tidak mengizinkan deklarasi bernama pada jenis delegate*. Jika kami menemukan ada kebutuhan yang signifikan untuk ini berdasarkan umpan balik dari penggunaan pelanggan, kami akan memeriksa solusi penamaan yang sesuai untuk penunjuk fungsi, tuple, generik, dll. Kemungkinan, ini akan serupa dengan saran lain seperti dukungan penuh untuk typedef dalam bahasa tersebut.

Pertimbangan Masa Depan

delegasi statis

Ini merujuk pada proposal untuk memungkinkan deklarasi jenis delegate yang hanya dapat merujuk pada anggota static. Keuntungannya adalah bahwa instans delegate tersebut dapat menjadi bebas alokasi dan lebih baik dalam skenario sensitif performa.

Jika fitur penunjuk fungsi diimplementasikan, proposal static delegate kemungkinan akan ditutup. Keuntungan yang diusulkan dari fitur itu adalah sifat bebas alokasi. Namun, penyelidikan baru-baru ini menemukan bahwa hal tersebut tidak mungkin dicapai karena pembongkaran proses perakitan. Harus ada handel yang kuat dari static delegate ke metode yang dirujuknya untuk menjaga perakitan agar tidak dibongkar dari bawahnya.

Untuk mempertahankan setiap instans static delegate diperlukan untuk mengalokasikan handle baru yang bertentangan dengan tujuan proposal. Ada beberapa desain di mana alokasi dapat diamortisasi ke satu alokasi per lokasi panggilan tetapi itu agak kompleks dan tampaknya tidak sepadan dengan pertukaran yang harus diterima.

Itu berarti pengembang pada dasarnya harus memutuskan antara trade off berikut:

  1. Keamanan dalam menghadapi pembongkaran rakitan: ini membutuhkan alokasi dan karenanya delegate sudah menjadi pilihan yang memadai.
  2. Tidak ada jaminan keamanan saat pembongkaran rakitan: gunakanlah delegate*. Ini dapat dibungkus dalam struct untuk memungkinkan penggunaan di luar konteks unsafe dalam kode lainnya.