Saya mengevaluasi perpustakaan yang API publiknya saat ini terlihat seperti ini:
libengine.h
/* Handle, used for all APIs */ typedef size_t enh; /* Create new engine instance; result returned in handle */ int en_open(int mode, enh *handle); /* Start an engine */ int en_start(enh handle); /* Add a new hook to the engine; hook handle returned in h2 */ int en_add_hook(enh handle, int hooknum, enh *h2);
Perhatikan bahwa itu enh
adalah pegangan umum, digunakan sebagai pegangan untuk beberapa tipe data yang berbeda ( mesin dan kait ).
Secara internal, sebagian besar API ini tentu saja melemparkan "pegangan" ke struktur internal yang telah mereka malloc
:
engine.c
struct engine { // ... implementation details ... }; int en_open(int mode, *enh handle) { struct engine *en; en = malloc(sizeof(*en)); if (!en) return -1; // ...initialization... *handle = (enh)en; return 0; } int en_start(enh handle) { struct engine *en = (struct engine*)handle; return en->start(en); }
Secara pribadi, saya benci menyembunyikan hal-hal di belakang typedef
, terutama ketika itu mengganggu keamanan jenis. (Diberikan enh
, bagaimana saya tahu apa yang sebenarnya dimaksud?)
Jadi saya mengirimkan permintaan tarik, menyarankan perubahan API berikut (setelah memodifikasi seluruh perpustakaan agar sesuai):
libengine.h
struct engine; /* Forward declaration */
typedef size_t hook_h; /* Still a handle, for other reasons */
/* Create new engine instance, result returned in en */
int en_open(int mode, struct engine **en);
/* Start an engine */
int en_start(struct engine *en);
/* Add a new hook to the engine; hook handle returned in hh */
int en_add_hook(struct engine *en, int hooknum, hook_h *hh);
Tentu saja, ini membuat implementasi API internal terlihat jauh lebih baik, menghilangkan gips, dan menjaga keamanan jenis ke / dari perspektif konsumen.
libengine.c
struct engine
{
// ... implementation details ...
};
int en_open(int mode, struct engine **en)
{
struct engine *_e;
_e = malloc(sizeof(*_e));
if (!_e)
return -1;
// ...initialization...
*en = _e;
return 0;
}
int en_start(struct engine *en)
{
return en->start(en);
}
Saya lebih suka ini karena alasan berikut:
- Keamanan tipe ditambahkan
- Kejelasan jenis dan tujuan yang ditingkatkan
- Gips dan
typedef
s dihapus - Ini mengikuti pola yang disarankan untuk jenis buram di C
Namun, pemilik proyek menolak keras permintaan tarik (diparafrasekan):
Secara pribadi saya tidak suka ide mengekspos
struct engine
. Saya masih berpikir cara saat ini lebih bersih & lebih ramah.Awalnya saya menggunakan tipe data lain untuk pegangan kait, tetapi kemudian memutuskan untuk beralih menggunakan
enh
, jadi semua jenis pegangan berbagi tipe data yang sama agar tetap sederhana. Jika ini membingungkan, kita tentu bisa menggunakan tipe data lain.Mari kita lihat apa pendapat orang lain tentang PR ini.
Perpustakaan ini saat ini dalam tahap beta pribadi, jadi tidak ada banyak kode konsumen yang perlu dikhawatirkan (belum). Juga, saya sedikit mengaburkan nama.
Bagaimana cara menangani buram lebih baik daripada yang disebut, struct buram?
Catatan: Saya mengajukan pertanyaan ini di Code Review , di mana ditutup.
sumber
Jawaban:
Mantra "sederhana adalah lebih baik" telah menjadi terlalu dogmatis. Sederhana tidak selalu lebih baik jika menyulitkan hal-hal lain. Majelis sederhana - setiap perintah jauh lebih sederhana daripada perintah bahasa tingkat tinggi - namun program Majelis lebih kompleks daripada bahasa tingkat lebih tinggi yang melakukan hal yang sama. Dalam kasus Anda, jenis gagang seragam
enh
membuat jenis lebih sederhana dengan biaya membuat fungsi menjadi kompleks. Karena biasanya jenis proyek cenderung tumbuh dalam tingkat sub-linear dibandingkan dengan fungsinya, karena proyek semakin besar, Anda biasanya lebih suka jenis yang lebih kompleks jika dapat membuat fungsi lebih sederhana - jadi dalam hal ini pendekatan Anda tampaknya menjadi yang benar.Penulis proyek khawatir bahwa pendekatan Anda " mengekspos
struct engine
". Saya akan menjelaskan kepada mereka bahwa itu tidak mengekspos struct itu sendiri - hanya fakta bahwa ada struct bernamaengine
. Pengguna perpustakaan sudah harus menyadari tipe itu - mereka perlu tahu, misalnya, bahwa argumen pertamaen_add_hook
adalah tipe itu, dan argumen pertama adalah tipe yang berbeda. Jadi sebenarnya membuat API lebih kompleks, karena alih-alih memiliki fungsi "tanda tangan" dokumen jenis ini perlu didokumentasikan di tempat lain, dan karena kompiler tidak dapat lagi memverifikasi jenis untuk programmer.Satu hal yang harus diperhatikan - API baru Anda membuat kode pengguna sedikit lebih rumit, karena alih-alih menulis:
Mereka sekarang membutuhkan sintaks yang lebih kompleks untuk menyatakan pegangan mereka:
Solusinya, bagaimanapun, cukup sederhana:
dan sekarang Anda dapat langsung menulis:
sumber
struct
secara tegas tidak disarankan.typedef struct engine engine;
dan menggunakanengine*
: Satu nama kurang diperkenalkan, dan itu membuatnya jelas itu seperti peganganFILE*
.Tampaknya ada kebingungan di kedua sisi di sini:
struct
nama tidak mengekspos detailnya (hanya keberadaannya)Ada keuntungan menggunakan pegangan daripada bare pointer, dalam bahasa seperti C, karena menyerahkan pointer memungkinkan manipulasi langsung pointee (termasuk panggilan ke
free
) sedangkan menyerahkan pegangan mengharuskan klien pergi melalui API untuk melakukan tindakan apa pun .Namun, pendekatan memiliki satu jenis pegangan, yang didefinisikan melalui a
typedef
bukan jenis aman, dan dapat menyebabkan banyak kesedihan.Saran pribadi saya adalah pindah ke tipe pegangan yang aman, yang saya pikir akan memuaskan Anda berdua. Ini dilakukan agak sederhana:
Sekarang, seseorang tidak dapat secara tidak sengaja melewati
2
sebagai pegangan atau seseorang dapat secara tidak sengaja melewati pegangan ke sapu dimana pegangan ke mesin diharapkan.Itu adalah kesalahan Anda: sebelum terlibat dalam pekerjaan yang signifikan di pustaka sumber terbuka, hubungi penulis / pengelola untuk membahas perubahan di muka . Ini akan memungkinkan Anda berdua untuk menyetujui apa yang harus dilakukan (atau tidak dilakukan), dan menghindari pekerjaan yang tidak perlu dan frustrasi yang dihasilkan dari itu.
sumber
struct file
dariint fd
. Ini tentunya berlebihan untuk IMO pustaka mode pengguna.3
) dibebaskan, kemudian objek baru dibuat dan sayangnya ditugaskan indeks3
lagi. Sederhananya, sulit untuk memiliki mekanisme seumur hidup objek aman di C kecuali penghitungan referensi (bersama dengan konvensi tentang kepemilikan bersama terhadap objek) dibuat menjadi bagian eksplisit dari desain API.Berikut adalah situasi di mana pegangan buram diperlukan;
Ketika pustaka memiliki dua atau lebih tipe struct yang memiliki bagian header yang sama dari bidang, seperti "tipe" di atas, tipe-tipe struct ini dapat dianggap memiliki struct induk yang sama (seperti kelas dasar dalam C ++).
Anda dapat mendefinisikan bagian header sebagai "mesin struct", seperti ini;
Tapi itu adalah keputusan opsional karena tipe-gips diperlukan terlepas dari menggunakan engine struct.
Kesimpulan
Dalam beberapa kasus, ada alasan mengapa pegangan buram digunakan sebagai pengganti struct buram.
sumber
switch
di tempat pertama, dengan menggunakan "fungsi virtual" mungkin ideal, dan menyelesaikan seluruh masalah.Manfaat yang paling jelas dari pendekatan pegangan adalah bahwa Anda dapat memodifikasi struktur internal tanpa melanggar API eksternal. Memang, Anda masih harus memodifikasi perangkat lunak klien, tetapi setidaknya Anda tidak mengubah antarmuka.
Hal lain yang dilakukannya adalah menyediakan kemampuan untuk memilih dari berbagai jenis yang berbeda saat runtime, tanpa harus menyediakan antarmuka API eksplisit untuk masing-masing. Beberapa aplikasi, seperti pembacaan sensor dari beberapa jenis sensor yang berbeda di mana masing-masing sensor sedikit berbeda dan menghasilkan data yang sedikit berbeda, merespons dengan baik terhadap pendekatan ini.
Karena bagaimanapun Anda akan menyediakan struktur untuk klien Anda, Anda mengorbankan sedikit keamanan jenis (yang masih dapat diperiksa saat runtime) untuk API yang jauh lebih sederhana, meskipun membutuhkan pengecoran.
sumber
Déjà vu
Saya menemui skenario yang sama persis , hanya dengan beberapa perbedaan yang halus. Kami sudah, di SDK kami, banyak hal seperti ini:
Usul belaka saya adalah membuatnya cocok dengan tipe internal kami:
Bagi pihak ketiga yang menggunakan SDK, seharusnya tidak ada bedanya sama sekali. Ini adalah jenis buram. Siapa peduli? Itu tidak berpengaruh pada ABI * atau kompatibilitas sumber, dan menggunakan rilis baru SDK memerlukan plugin yang harus dikompilasi ulang.
* Perhatikan bahwa, seperti yang ditunjukkan oleh gnasher, sebenarnya bisa ada kasus di mana ukuran sesuatu seperti pointer ke struct dan void * sebenarnya mungkin ukuran yang berbeda dalam hal ini akan mempengaruhi ABI. Seperti dia, saya belum pernah menemukannya dalam latihan. Tapi dari sudut pandang itu, yang kedua sebenarnya bisa meningkatkan portabilitas dalam beberapa konteks yang tidak jelas, jadi itu alasan lain untuk memilih yang kedua, meskipun mungkin bisa diperdebatkan bagi kebanyakan orang.
Bug Pihak Ketiga
Selain itu, saya bahkan memiliki lebih banyak penyebab daripada keamanan jenis untuk pengembangan / debugging internal. Kami sudah memiliki sejumlah pengembang plugin yang memiliki bug dalam kode mereka karena dua pegangan yang sama (
Panel
danPanelNew
, yaitu) keduanya menggunakanvoid*
typedef untuk pegangan mereka, dan mereka secara tidak sengaja melewati gagang yang salah ke tempat yang salah karena hanya menggunakanvoid*
untuk semuanya. Jadi itu sebenarnya menyebabkan bug di sisi mereka yang menggunakanSDK. Bug mereka juga menghabiskan waktu tim pengembangan internal, karena mereka akan mengirim laporan bug yang mengeluhkan bug di SDK kami, dan kami harus men-debug plugin dan menemukan bahwa itu sebenarnya disebabkan oleh bug di plugin yang melewati pegangan yang salah ke tempat yang salah (yang mudah diizinkan tanpa peringatan ketika setiap pegangan adalah alias untukvoid*
atausize_t
). Jadi kami tidak perlu membuang waktu untuk menyediakan layanan debug untuk pihak ketiga karena kesalahan yang disebabkan oleh keinginan kami untuk kemurnian konseptual dalam menyembunyikan semua informasi internal, bahkan hanya nama - nama internal kamistructs
.Menjaga Typedef
Perbedaannya adalah bahwa saya mengusulkan agar kita tetap berpegang pada
typedef
diam, bukan untuk memiliki klien menulisstruct SomeVertex
yang akan mempengaruhi kompatibilitas sumber untuk rilis plugin masa depan. Sementara saya pribadi menyukai gagasan tidak mengetikstruct
dalam C, dari perspektif SDK,typedef
dapat membantu karena seluruh titik opacity. Jadi saya sarankan untuk merelaksasi standar ini hanya untuk API yang terbuka untuk umum. Untuk klien yang menggunakan SDK, seharusnya tidak masalah apakah pegangan adalah pointer ke struct, integer, dll. Satu-satunya hal yang penting bagi mereka adalah bahwa dua pegangan berbeda tidak alias tipe data yang sama sehingga mereka tidak salah memasukkan pegangan yang salah ke tempat yang salah.Ketikkan Informasi
Yang paling penting untuk menghindari casting adalah untuk Anda, para pengembang internal. Jenis estetika menyembunyikan semua nama internal dari SDK adalah beberapa estetika konseptual yang datang dengan biaya yang signifikan dari kehilangan semua jenis informasi, dan mengharuskan kita untuk menaburkan gips ke dalam pengadu kita untuk mendapatkan informasi penting. Sementara seorang programmer C sebagian besar harus terbiasa dengan ini dalam C, membutuhkan ini sia-sia hanya meminta masalah.
Cita-cita Konseptual
Secara umum, Anda ingin berhati-hati terhadap tipe-tipe pengembang yang menaruh gagasan konseptual tentang kemurnian yang jauh di atas semua kebutuhan praktis dan harian. Itu akan mendorong pemeliharaan basis kode Anda ke tanah dalam mencari cita-cita utopis, membuat seluruh tim menghindari lotion berjemur di padang pasir karena takut itu tidak wajar dan dapat menyebabkan kekurangan vitamin D sementara setengah kru sekarat karena kanker kulit.
Preferensi Pengguna-Akhir
Bahkan dari sudut pandang pengguna-akhir yang ketat dari mereka yang menggunakan API, apakah mereka lebih suka kereta buggy atau API yang berfungsi dengan baik tetapi memperlihatkan beberapa nama yang mereka tidak bisa pedulikan sebagai gantinya? Karena itulah pertukaran praktis. Kehilangan informasi jenis yang tidak perlu di luar konteks umum meningkatkan risiko bug, dan dari basis kode skala besar dalam pengaturan tim selama beberapa tahun, hukum Murphy cenderung cukup berlaku. Jika Anda secara berlebihan meningkatkan risiko bug, kemungkinan Anda setidaknya akan mendapatkan lebih banyak bug. Tidak perlu waktu terlalu lama dalam pengaturan tim besar untuk menemukan bahwa setiap jenis kesalahan manusia yang dibayangkan pada akhirnya akan berubah dari potensi menjadi kenyataan.
Jadi mungkin itu pertanyaan yang diajukan kepada pengguna. "Apakah Anda lebih suka SDK buggier atau yang memperlihatkan beberapa nama buram internal yang Anda bahkan tidak akan pernah peduli?" Dan jika pertanyaan itu tampaknya dikotomi palsu, saya akan mengatakan lebih banyak pengalaman di tim dalam skala yang sangat besar diperlukan untuk menghargai fakta bahwa risiko yang lebih tinggi untuk bug pada akhirnya akan memanifestasikan bug nyata dalam jangka panjang. Tidak masalah seberapa besar pengembang percaya diri dalam menghindari bug. Dalam pengaturan tim-lebar, itu membantu lebih banyak untuk berpikir tentang link terlemah, dan setidaknya cara termudah dan tercepat untuk mencegah mereka dari tersandung.
Usul
Jadi saya sarankan kompromi di sini yang masih akan memberi Anda kemampuan untuk mempertahankan semua manfaat debug:
... bahkan dengan biaya mengetik
struct
, akankah itu benar-benar membunuh kita? Mungkin tidak, jadi saya merekomendasikan beberapa pragmatisme di pihak Anda juga, tetapi lebih kepada pengembang yang lebih suka untuk membuat debugging lebih sulit secara eksponensial dengan menggunakan disize_t
sini dan melakukan casting ke / dari integer tanpa alasan yang baik kecuali untuk menyembunyikan informasi yang sudah ada. % disembunyikan bagi pengguna dan tidak mungkin membahayakan lebih darisize_t
.sumber
void*
atausize_t
dan kembali sebagai alasan lain untuk menghindari casting berlebihan. Saya agak menghilangkannya karena saya juga tidak pernah melihatnya dalam praktik mengingat platform yang kami targetkan (yang selalu merupakan platform desktop: linux, OSX, Windows).typedef struct uc_struct uc_engine;
Saya menduga alasan sebenarnya adalah kelembaman, itu yang selalu mereka lakukan dan itu berhasil jadi mengapa mengubahnya?
Alasan utama yang bisa saya lihat adalah bahwa pegangan yang buram memungkinkan desainer meletakkan apa pun di belakangnya, bukan hanya struct. Jika API kembali dan menerima beberapa jenis buram mereka semua terlihat sama dengan penelepon dan tidak pernah ada masalah kompilasi atau kompilasi yang diperlukan jika perubahan cetak halus. Jika en_NewFlidgetTwiddler (handle ** newTwiddler) berubah untuk mengembalikan pointer ke Twiddler alih-alih handle, API tidak berubah dan kode baru apa pun akan diam-diam menggunakan pointer di mana sebelum menggunakan handle. Juga, tidak ada bahaya dari OS atau hal lain diam-diam "memperbaiki" pointer jika melewati melewati batas.
Kerugian dari itu, tentu saja, adalah bahwa penelepon dapat memberi makan apa saja ke dalamnya. Anda memiliki hal 64 bit? Dorong ke dalam slot 64 bit di panggilan API dan lihat apa yang terjadi.
Keduanya mengkompilasi tetapi saya yakin hanya satu dari mereka melakukan apa yang Anda inginkan.
sumber
Saya percaya bahwa sikap tersebut berasal dari filosofi lama untuk mempertahankan C library API dari penyalahgunaan oleh pemula.
Khususnya,
memcpy
menambah data buram atau byte atau kata-kata di dalam struct. Pergi meretas.Penanggulangan tradisional lama adalah untuk:
void*
struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);
Ini hanya untuk mengatakan ia memiliki tradisi panjang; Saya tidak punya pendapat pribadi apakah itu benar atau salah.
sumber