2 puan yazan GN⁺ 2025-01-13 | 1 yorum | WhatsApp'ta paylaş
  • C23’e kadar standart C değişmeye devam etti, ancak Walter Bright sabit ifade değerlendirmesi, ileriye başvuru ve header bağımlılığı gibi eski rahatsızlıkların hâlâ geliştirme akışını engellediğini düşünüyor
  • D derleyicisine yerleşik C derleyicisi ImportC, yeni bir uygulama olmanın avantajını kullanarak constant-expression konumlarında koşulları karşılayan fonksiyonları derleme zamanında çalıştırabiliyor
  • CTFE mümkün olduğunda _Static_assert(sum(3, 4) == 7,...) gibi birim testleri, ayrı bir çalıştırılabilir dosya olmadan her derlemede çalıştırılabiliyor
  • Standart C’deki bildirim sırası kısıtı, ileri bildirim tekrarına ve kodun ters sırada yerleştirilmesine yol açıyor; ImportC ise global bildirim sırasına daha az bağlı bir yaklaşım benimsiyor
  • __import dex; gibi .c dosyasındaki bildirimleri doğrudan içe aktarmak, ayrı .h dosyalarını azaltıyor ve header ile uygulama arasındaki uyumsuzluklardan doğan hata ayıklama yükünü de düşürüyor

Standart C’de Kalan Rahatsızlıklar

  • Standart C, C23’e kadar düzenli olarak iyileştirildi; ancak Walter Bright hâlâ düzeltilmemiş kısımlar kaldığını düşünüyor
  • Dlang topluluğu, D programlama dili derleyicisinin içine bir C derleyicisi yerleştirdi ve bu derleyiciye ImportC adını veriyor
  • ImportC, baştan yeni yazıldığı için modern derleyici teknolojileriyle C’nin eksik yönlerini ele alma alanına sahipti
  • Yazıda ele alınan başlıklar dört tane
    • constant-expression değerlendirmesi
    • Derleme zamanında birim test
    • Bildirimlerde ileriye başvuru
    • Bildirimleri içe aktarma

Derleme Zamanında Fonksiyon Çalıştırma ve Sabit İfade Değerlendirmesi

  • C, basit ifadeleri sabit katlama (constant folding) ile derleme zamanında hesaplayabilir; ancak standart C’de fonksiyonlar derleme zamanında çalıştırılamaz
  • Örnek kod enum E { A = 3, B = 4, C = sum(5, 6) }; gcc ile derlendiğinde, C numaralandırıcısının değerinin tamsayı sabiti olmadığına dair bir hata oluşur
  • ImportC aynı kodu derleyebilir
  • Yaklaşım, C sözdiziminde constant-expression görülen her yerde derleyicinin fonksiyonları derleme zamanında çalıştırabilmesi gerektiği yönünde
  • Ancak ilgili fonksiyon I/O, değiştirilebilir global değişkenlere erişim veya sistem çağrıları gibi işler yapmamalı

Her Derlemede Çalışan Birim Testler

  • Derleme zamanı fonksiyon değerlendirmesi olan CTFE mümkün hâle gelirse, birim test yaklaşımı da değişebilir
  • C kodunda birim testlerin seyrek olmasının nedeni, ayrı build target ve çalıştırılabilir dosya oluşturup çalıştırma zahmetidir
  • Örnek kod _Static_assert(sum(3, 4) == 7, "test #1"); gcc ile derlendiğinde, statik assertion içindeki ifadenin sabit olmadığına dair hata oluşur
  • ImportC bu kodu derleyebilir
  • Bu yöntem, derleme zamanında çalıştırılabilen fonksiyonların birim testlerini basitleştirir
    • Ayrı build gerekmez
    • Ek çalışma gerekmez
    • Kod her derlendiğinde test çalışır
  • ImportC test suite’inde de bu yöntem yaygın biçimde kullanılır

İleriye Başvurunun Yarattığı Bildirim Tekrarı

  • Standart C’de derleyici yalnızca sözcüksel olarak daha önce gelen bildirimleri bildiği için ileriye başvuruya izin verilmez
  • Örnek kodda floo() daha sonra tanımlanan dex() fonksiyonunu önce çağırırsa, gcc dex için örtük bildirim ile gerçek tanımın tiplerinin çakıştığını belirten bir hata verir
  • floo ve dex sırası değiştirilirse aynı kod sorunsuz derlenir
  • ImportC, global bildirimler hangi sırayla gelirse gelsin kabul edebilir
  • İleriye başvuru olmadığında, her ileri tanım için ayrı bir bildirim eklemek gerekir
    • Önce char dex(char *s, int i); gibi bir bildirim yazılır
    • Ardından fonksiyon tanımı tekrar yazılmalıdır
  • Bu yöntem gereksiz tekrar işidir ve programcıyı leaf fonksiyonları üste, global arayüz fonksiyonlarını alta koyan ters sıra yerleşimi yapmaya iter
  • Bu yerleşimin, bir gazete haberini aşağıdan yukarı okumaya benzediği ve mantıklı olmadığı eleştirisi yapılıyor

Header Dosyası Olmadan Bildirimleri İçe Aktarma

  • C’de harici bir modül kullanmak için genellikle .h dosyasında bildirimler oluşturmak ve bunları .c dosyasında include etmek gerekir
  • Örnek yapı şu şekildedir
    • floo.c, dex.h dosyasını include eder ve dex() fonksiyonunu çağırır
    • dex.h, char dex(char *s, int i); bildirimini içerir
    • dex.c, dex.h dosyasını include eder ve dex() fonksiyonunu tanımlar
  • Her harici modül için .h dosyası oluşturmak tekrar işini artırır
  • .h dosyası .c dosyasıyla tam olarak eşleşmezse, neyin yanlış olduğunu bulmak çok zaman alabilir
  • Önerilen yöntem, dex.c dosyasını doğrudan içe aktarmaktır
    • floo.c içinde __import dex; kullanılır
    • dex.c içinde yalnızca char dexx(char *s, int i) { return s[i]; } gibi tanımlar bulunur
  • Bu yöntemle .h dosyası yazmaya hiç gerek kalmaz
  • ImportC bu yöntemi de destekler

İlgili Belgeler

1 yorum

 
GN⁺ 2025-01-13
Hacker News yorumları
  • C dışında bir dil kullanırken en çok özlediğim şeylerden biri header dosyaları
    Kamusal/özel, arayüz/uygulama ayrımının çok net olması C kodunda özellikle hoşuma gidiyor. Bir kütüphanenin yalnızca .h dosyasına göz atarak nasıl kullanılacağını anlayabilmek iyi; genelde .h içinde kullanım dokümantasyonu bulunur ve .c ile tekrar etmez. Dokümantasyonu .c içine koymak da mümkün ama kullanıcı açısından arayüzü okumak çok daha az konforlu hale gelir

    • Bu iddia bana hep garip gelmiştir; diğer diller bu sorunu araçlarla çözüyor ve bence bu daha iyi
      Örneğin Rust’ta bir kütüphanenin arayüzünü ve kullanımını görmek için cargo doc --open yeterli. Tüm herkese açık API otomatik olarak aranabilir biçimde oluşturulur; header ile kaynak arasında kodu tekrarlamaya ya da elle yönetmeye gerek kalmaz
    • Header dosyalarının, 70’lerin kaynak kısıtlı platformlarıyla başa çıkmak için yapılmış zayıf bir hack’e daha yakın olduğunu düşünüyorum
      Yalnızca teamüllere uyulduğunda düzgün çalışıyorlar; Ada gibi arayüz ve uygulama belirtimleri iyi tasarlanmış, tekrarlı ayrıştırma gerektirmeyen dillerle karşılaştırılınca sönük kalıyorlar. C kullanmayı seviyorum ama bu kısmın daha iyi tasarlanmış olması gerekirdi
    • Hem katılıyorum hem katılmıyorum
      C’deki header dosyaları metni olduğu gibi araya ekleme yöntemi olduğundan “kabaca çalışıyor gibi görünen” bir düzeyde. Buna karşılık Ada’da paket ve paket gövdesi kavramları vardı; paket header dosyasına, paket gövdesi de uygulamaya karşılık geliyordu. Uzun zaman önce Ada kullandığımda, paket gövdesi uygulaması hazır olmasa bile herkes paket temelinde derleme yapabiliyordu; böylece uygulamadan önce de arayüz üzerinde uyum sağlanabiliyordu. Başka bir yönden bakınca Python’daki importun dosya sistemine doğal biçimde eşlenmesi ve C’nin include semantiğiyle uğraşmak zorunda bırakmaması nedeniyle “header dosyası” rolünde hoşuma gidiyor
    • Burada bir düşünme biçimi farkı var gibi görünüyor
      Header dosyaları olduğunda arayüzü uygulamadan farklı bir şey olarak düşünmeye başlıyorsunuz. Bu yüzden bu yaklaşıma alışkın olanlar, arayüzün araç tarafından üretildiği fikrinden rahatsız oluyor. Arayüz, uygulamanın bir yan ürünü değil; ayrıca ve bilinçli olarak tasarlanan bir şeydir ve bazıları için uygulamadan daha önemlidir. Tersine, otomatik üretilen dokümantasyona alışkın olanlar, arayüzün uygulama kaynağı adlı tek doğruluk kaynağından üretilmemesinden rahatsız olur. Ayrı arayüz dosyası olan ve olmayan dilleri uzun süre kullandıkça herkes kendi kampına yerleşir ve karşıt biçimde düşünmenin hissini unutur
    • Derlenen modül dillerinin çoğunda bu zaten mümkün
      Modula-2, Modula-3, Ada, Standard ML, Caml Light, OCaml, F#, D gibi ayrı tutarak ya da Object Pascal, D, Haskell, Java, C#, F#, Swift, Go, Rust gibi metin veya grafik araçlarla üreterek. Hepsi daha güçlü tiplere, daha hızlı derlemeye ve düzgün ad alanlarına sahip. Rust ve Swift toolchain’lerinde hâlâ elden geçirilmesi gereken yerler var ama C araçları, Bell Labs dışında olup bitenlere kıyasla her zaman ilkel kaldı. AT&T bunu ticari olarak değerlendirebilseydi tarih farklı olurdu; sonuçta olgun portakallar yerine bedava limonlar almış olduk. Yine de C için TypeScript benzeri bir şey yaptılar ve bugün sınır denetimli koleksiyon tiplerini ve düzgün modülleri de destekliyor
  • Bu alanda yazarın çok daha fazla bilgi ve deneyimi olduğundan, aşağıdaki sorunları nasıl çözeceğini merak ediyorum
    Sabit ifade değerlendirmesi yalnızca çeviri birimi içinde yapılırsa basitleşir ama kod tekrarı olmadan yapılabilecek şeyler ciddi biçimde sınırlanır. Derleme zamanı birim testleri, testler makro olarak ifade edilebiliyorsa bir ölçüde mümkün; önceki madde de eklenirse kolaylaşır. Bildirimlerin ileri referansı büyük tepki çekebilir; çünkü derleyici 1 geçişten 2 geçişe değişir ve bunun performans etkisi olur. Dev kod tabanlarını derlerken çeviri birimi paralelleştirmesi de yapanlar için bunu kabul etmek zor olabilir. Bildirim içe aktarma uyumluluğu bozan bir değişikliktir. C’de bir değişken tanımladıktan sonra bir .c dosyasını import edip, değişkeni değiştirip aynı .cyi tekrar import ederek şablona benzer bir şey uyguladığım olmuştu. SQLite C Amalgamation’ın önüne çeşitli tanımlar koyup iç fonksiyonları açığa çıkarmak için fonksiyon eklediğim de olmuştu; bu kullanım örneklerinin hepsi bozulacak gibi görünüyor. Bu sorunlar için bir çözüm olup olmadığını merak ediyorum

    • Değerlendirilen fonksiyonun kaynak kodunu derleyicinin görebilmesi gerektiği doğru
      C’de bu #include ile yapılabilir; D’de ise gerekli kodun bulunduğu modül import edilerek işlenir. Testleri makro olarak ifade etme yöntemi, bir fonksiyonu test etmek istediğiniz durumlar için uygun değildir. Örnek, anlaşılmayı kolaylaştırmak için basit tutuldu; gerçek kullanım çok daha karmaşık hale gelebilir. Performans açısından D, C derleyicisinden daha hızlı derlenir; başlıca nedeni C önişlemcisinin birden fazla geçiş gerektiren fosilleşmiş bir yapı olması ve D’nin #include yerine import kullanarak .h dosyalarını tekrar tekrar derlememesidir. D’nin stratejisi ayrıştırma ile anlamsal analizi ayırmaktır; biraz yavaş olabilir ama yinelenen bildirimleri yeniden derleyip tekilleştirme maliyeti yoktur. Derleme zamanı fonksiyon yürütme çok kullanılırsa darboğaz olabilir; hafif kullanıldığında ise performans gayet iyidir. C’de şablonları hack’leyerek uyguluyorsanız zaten dilin ötesine geçmişsiniz demektir ve daha güçlü bir dile ihtiyacınız vardır. D, en üst düzey metaprogramlamaya sahiptir ve diğer şablon dilleri de çoğu zaman D’nin yolundan ilerler
    • Kişisel olarak ileri referansları kod okumayı zorlaştırdığı için sevmiyorum
      Bağımlılık grafiğinin artık topolojik sıralama düzeninde yer aldığına güvenemiyorsunuz
    • Diğer diller header dosyaları veya ileri bildirimler olmadan da iyi çalışıyor gibi görünüyor; bu yüzden neden tepki olduğunu anlamakta zorlanıyorum
      Modern C derleyicilerinin gerçekten hâlâ tek geçişli olup olmadığını da merak ediyorum
  • Yazıdaki sabit ifade değerlendirmesi örneği epey basit, ama daha karmaşık durumlarda derleyicinin hızı ve bellek kullanımı ciddi ölçüde kötüleşir; bundan yararlanmak için de bir sanal makine gerekecek gibi görünüyor.
    Bu yüzden bunun standarda koymak için “fazla karmaşık” görülmüş olabileceğini anlayabiliyorum. C++20’de tanımlanan tuhaf karışım yerine C++ veya C’nin bildirimleri içe aktarma yönüne gitmesini isterdim. Örneğin #import "string.c" as str gibi bir modülü bir sembol adıyla içe aktarıp dosyadaki tüm statik olmayan sembollere str.trim(" Hello World "); şeklinde erişilebilmesi gibi. Ayrı olarak, __import dex; gibi dosya yolunun açıkça belirtilmediği şeyleri sevmiyorum. Bu durumda dex.d mi yoksa dex.c mi içe aktarılıyor bilemezsiniz

    • Diğer popüler diller de bunu yapabiliyor; D’de derleme zamanı işlev yürütme çok popüler ve kullanışlı bir özellik
      Çok kullanılırsa elbette derleme süresi ve bellek harcar. Sanal makine konusuna gelince, sabit katlama zaten başlı başına bir sanal makinedir; burada yapılan, buna işlev çağrısı işlemeyi genişletmek olur. C’nin semantiği basit olduğu için o kadar kötü değil. Önerdiğiniz import yöntemi D import’un yaptığı işle neredeyse aynı: https://dlang.org/spec/module.html#import-declaration. dex.d mi dex.c mi sorunu gerçekten ortaya çıkar; bunun yanıtı import yolu ayarıdır. C derleyicilerindeki include yoluna benzer
    • Çoğu gerçek C derleyicisi optimizasyon için zaten bir ölçüde derleme zamanı değerlendirmesi yapıyor olmalı
      C’de zaten sabit ifadeler var. Daha büyük engel, derleyicinin işlevin kaynak koduna erişmesi gerektiği; bu yüzden muhtemelen aynı çeviri birimindeki işlevlerle sınırlı kalır. Ayrıca çeşitli derleyici temsilcilerinden oluşan bir komitenin böyle bir sabit değerlendirmenin semantiği üzerinde anlaşması gerektiği insan tarafı da çok daha büyük bir sorun olabilir
    • Bu aslında C++’ın constexpr özelliğiyle neredeyse bire bir aynı görünüyor
      Neredeyse tüm C derleyicileri zaten aynı zamanda C++ derleyicisi de olduğundan, C’de constexpr işlevleri ve değerlendirmeyi desteklemenin o kadar da kötü olup olmayacağını merak ediyorum
  • C kodu için her zaman birim testleri yazarım
    İyi bir derleme sistemi kullanır ve biraz boilerplate’e katlanırsanız zor değil. npy kütüphanesinin testleri test_load_uint8() içinde npy_load("tests/npy/uint8.npy") çağırır, boyutları, büyüklüğü ve tipi assert eder, ardından npy_free yapar; main içinde de PRINT_RUN(test_load_uint8); gibi çalıştırılır. Ön işlemciyle testlerin bir kısmı üretilebilir, ama ben basit tutmayı tercih ederim

    • O işlev I/O yapıyor gibi görünüyor, bu yüzden derleme zamanı testi olarak çalışmaz
      ImportC derleyicisinin birim test örnekleri _Static_assert(sizeof(struct S22079){1,2,3} == sizeof(int)*3, "ok"); ve _Static_assert(sizeof(struct S22079){1,2,3}.a == sizeof(int), "ok"); gibi. Semantiği derleme zamanında kontrol ettiği için bağlamaya ve çalıştırmaya gerek yok. Test sayısı arttıkça bu yöntem epey hızlanır; test paketi ne kadar hızlı dönerse üretkenlik de o kadar artar
    • Ben de neredeyse aynı şekilde yapıyorum
      Bir dönem gösterişli birim test framework’lerini derinlemesine kurcaladım, ama pek fayda sağlamadıklarını fark edip neredeyse aynı biçime yerleştim. Başkalarının hoş karşılamayabileceği nokta, statik işlevleri çağırmak için test edilen .c dosyasını isteyerek #include etmem. Yalnızca tek bir .c dosyası include ediyorum. Ayrıca #include öncesinde küçük bir ön işlemci işlemi yaparak, birinin “release modu”nda derleyip assert’leri devre dışı bırakması durumunu önlemek için NDEBUG’nin tanımlı olmamasını sağlıyorum
    • Testleri olabildiğince kolay çalıştırılabilir hale getirmenin, gerçekten testleri olan bir kod tabanına ulaşmada büyük yardımı olduğunu düşünüyorum
      Benzer bir şey kullanıyorum: https://github.com/ensisoft/detonator/blob/master/base/test_.... boost.test.minimal’dan çok şey ödünç aldım; aslında tek header’dı, ama yıllar içinde tek bir çeviri birimi daha eklemek gerekti. Kod tabanını testlerin her zaman geçtiği bir durumda tutarsanız, test aracının hata raporlaması ya da hata toleransı gibi karmaşıklıklara çok daha az ihtiyaç kaldığı sonucuna vardım
  • Derleme zamanı birim testleri, “kullanılmayan import/değişken/sonuç” durumlarını uyarı değil hata yapmak kadar kötü bir fikir
    Geliştiricinin elinden kontrolü alan ve sonunda işi bitirmek için bürokratik prosedürlerden geçmeye zorlayan bir “dadı özelliği”. Bu tür derleme başarısızlığı testleri “artık bitti gibi” derlemeleri için iyi olabilir, ama derlemelerin %99’unu oluşturan “hâlâ üzerinde çalışıyorum” derlemelerine uygun değil. “Matkabı kaldırmadan masa testeresini kullanamazsın” demeye benziyor

    • Bu kısma katılmakta zorlanıyorum
      Bir düşünceyi ifade etmeye çalışırken derleme zamanı testi bunun yanlış olduğunu söylüyorsa, aslında o düşünce hâlâ eksik olabilir ya da o ifadenin tüm sonuçlarını yeterince düşünmemiş olabilirsiniz. Haskell’in tip denetimine benzer. Tip denetiminden geçmeyen bir program derlenemez ve programcıyı her zaman eksiksiz düşünceler ifade etmeye zorlar. Teorik olarak bu, daha iyi düşünülmüş programlara yol açabilir. Ancak programcıyı “geçersiz olduğunu biliyorum ama umursamıyorum” dediği köşeleri bile bulmaya zorladığı için yazmayı zorlaştırır
    • Bu tür derleme zamanı testleri static_assert’e daha yakın olabilir ve kütüphane işlevlerinin uyumsuz kullanımlarını yakalamada büyük değer taşır
      Bence oldukça iyi bir fikir
  • “leaf fonksiyonların önce, genel arayüz fonksiyonlarının en sonda gelmesi” bana ters geliyor
    Birçok nedenle kodu topolojik sıralama düzeninde yazmayı tercih ediyorum. Bu, bir fonksiyonun içinde kod yazma biçimine benziyor; modül içinde fonksiyonların nereye konacağı netleşiyor ve en önemlisi, modülün içindeki kod parçaları arasındaki döngüsel bağımlılıklar çok belirgin hale geliyor. Döngüsel bağımlılıkları pek sevmiyorum; kod tabanını daha da birbirine doluyor ve modülleri bağımsız birimler olarak anlamayı zorlaştırıyor. Python’da, çalıştırma zamanına kadar ortaya çıkmayan sorunlar da yaratabilir[0]; döngüsel import’lar o kadar yaygın ki mevcut tip denetleyicileri bu tanılamayı varsayılan olarak kapatıyor gibi görünüyor[1]. İleri referansı desteklemeyen C, OCaml, SML gibi diller, döngüsel bağımlılıklara “en az şaşkınlık ilkesi”ni uygulamayı sağlıyor. OCaml, let rec fn1 = .. and fn2 = .. gibi bildirilmedikçe fonksiyonlar arası özyinelemeli bağımlılığı bile yasaklıyor; yazarken biraz can sıkıcı olsa da okurken önemli bir bilgi haline geliyor
    [0]: https://gist.github.com/Mark24Code/2073470277437f2241033c200...
    [1]: https://microsoft.github.io/pyright/#/configuration?id=type-... (reportImportCycles bölümüne bakın)

  • Derleyicinin yalnızca sözcüksel olarak daha önce gelenleri bildiği için leaf fonksiyonların önce, genel arayüz fonksiyonlarının en sonda geldiği “aşağıdan yukarıya” bir sıra oluştuğuna dair bir açıklama var; ancak Python gibi ileri referansa izin veren dillerde de bu sıra yaygın[0]
    Bunun ileri referansa izin vermeyen dillerden kalma bir miras mı, yoksa belirli türden kodlar için gerçekten daha mantıklı bir yöntem mi olduğunu merak ediyorum
    [0] https://stackoverflow.com/a/73131538

    • Python’da ileri referans yok
      Sadece fonksiyon gövdesindeki tanımlayıcılar, fonksiyonun kendisi çalıştırılana kadar çözümlenmez; o anda da modül kapsamındaki her şey zaten tanımlanmış olur. Fonksiyon gövdesine herhangi bir ad koyup modülü yükleyerek bunu doğrudan kontrol edebilirsiniz
    • Benim kodumda herkese açık arayüz hep yukarı çıkar, uygulama ayrıntıları ise dosyanın sonuna gitme eğilimindedir
      Katı bir kuralım yok ama böyle okumanın daha temiz ve mantıklı olduğunu düşünüyorum. Özellikle uygulama büyükse, temelde ne yaptığını görmek için önce tamamını kaydırmak istemem. Başkaları nasıl yapıyor merak ediyorum
  • C’nin yapmasını istediğim “bariz şeyler” arasında işaretçi ve uzunluğu tutan slice tipi desteği, global durum kullanan API’lerin yeniden girilebilir ve mümkünse iş parçacığı güvenli sürümleri, Go veya Zig’deki defer ya da GCC’nin cleanup özniteliği gibi bir şeyin standartlaştırılması ve Unicode ile UTF-8 için taşınabilir destek var

    • Bunların çoğu dilin kendisinin yapması gereken şeyler değil de standart kütüphaneden istenen özellikler değil mi?
  • C söz diziminde sabit ifadenin geçtiği her yerde, fonksiyon I/O yapmadığı, değiştirilebilir global değişkenlere erişmediği, sistem çağrısı vb. gerçekleştirmediği sürece derleyicinin fonksiyonu derleme zamanında çalıştırabilmesi gerektiği fikri kolayca bozulabilir
    Çok uzun süren saf bir fonksiyon seçmek yeterli. Örneğin int busybeaver(int n) {...} n durumlu meşgul kunduz makinesinin azami ömrünü döndüren saf bir fonksiyonsa, int x = busybeaver(99); gibi bir kod sorun olur

  • C23’te constexpr var ama henüz fonksiyonlara uygulanamıyor
    Ancak bir öneri var: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2976.pdf