27 Mart 2025

C# Kodunu C++'a Taşıma: SmartPtr Uygulaması

En başından beri görev, birkaç milyon satıra kadar kod içeren birkaç projenin taşınmasını içeriyordu. Esasen, çevirici için teknik şartname “tüm bunların C++'a taşınmasını ve doğru şekilde çalışmasını sağlama” ifadesine indirgenmişti. C++ ürünlerini yayınlamaktan sorumlu olanların işi, kodu çevirmeyi, testleri çalıştırmayı, sürüm paketlerini hazırlamayı vb. içerir. Karşılaşılan sorunlar genellikle birkaç kategoriden birine girer:

  1. Kod C++'a çevrilmez - çevirici bir hatayla sonlanır.
  2. Kod C++'a çevrilir, ancak derlenmez.
  3. Kod derlenir, ancak bağlanmaz (link).
  4. Kod bağlanır ve çalışır, ancak testler başarısız olur veya çalışma zamanı çökmeleri meydana gelir.
  5. Testler başarılı olur, ancak yürütülmeleri sırasında ürünün işlevselliğiyle doğrudan ilgili olmayan sorunlar ortaya çıkar. Örnekler: bellek sızıntıları, düşük performans vb.

Bu listedeki ilerleme yukarıdan aşağıyadır — örneğin, çevrilen koddaki derleme sorunlarını çözmeden işlevselliğini ve performansını doğrulamak imkansızdır. Sonuç olarak, uzun süredir devam eden birçok sorun ancak CodePorting.Translator Cs2Cpp projesi üzerindeki çalışmaların sonraki aşamalarında keşfedildi.

Başlangıçta, nesneler arasındaki döngüsel bağımlılıklardan kaynaklanan basit bellek sızıntılarını düzeltirken, alanlara CppWeakPtr niteliğini uyguladık, bu da WeakPtr türünde alanlarla sonuçlandı. WeakPtr, lock() metodunu çağırarak veya örtük olarak (ki bu sözdizimsel olarak daha uygundur) SharedPtr'a dönüştürülebildiği sürece bu durum sorun yaratmadı. Ancak, daha sonra CppWeakPtr niteliği için özel bir sözdizimi kullanarak konteynerler içinde bulunan referansları da zayıf hale getirmek zorunda kaldık ve bu durum birkaç hoş olmayan sürprize yol açtı.

Benimsediğimiz yaklaşımla ilgili ilk sorun işareti, C++ perspektifinden bakıldığında, MyContainer<SharedPtr<MyClass>> ve MyContainer<WeakPtr<MyClass>>'ın iki farklı tür olmasıydı. Sonuç olarak, aynı değişkende saklanamazlar, aynı metoda geçirilemezler, ondan döndürülemezler vb. Başlangıçta yalnızca referansların nesne alanlarında nasıl saklanacağını yönetmek için tasarlanan nitelik, giderek daha garip bağlamlarda görünmeye başladı; dönüş değerlerini, argümanları, yerel değişkenleri vb. etkiledi. Bunu işlemekten sorumlu çevirici kodu her geçen gün daha karmaşık hale geldi.

İkinci sorun da beklemediğimiz bir şeydi. C# programcıları için, nesne başına tek bir ilişkisel koleksiyona sahip olmak doğal görünüyordu; bu koleksiyon hem mevcut nesnenin sahip olduğu ve başka türlü erişilemeyen nesnelere benzersiz referansları hem de üst nesnelere referansları içeriyordu. Bu, belirli dosya formatlarından okuma işlemlerini optimize etmek için yapılmıştı, ancak bizim için aynı koleksiyonun hem güçlü hem de zayıf referanslar içerebileceği anlamına geliyordu. İşaretçi türü, çalışma modunun nihai belirleyicisi olmaktan çıktı.

İşaretçi Durumunun Bir Parçası Olarak Referans Türü

Açıkçası, bu iki sorun mevcut paradigma içinde çözülemedi ve işaretçi türleri yeniden gözden geçirildi. Bu revize edilmiş yaklaşımın sonucu, iki değerden birini kabul eden bir set_Mode() metoduna sahip SmartPtr sınıfı oldu: SmartPtrMode::Shared ve SmartPtrMode::Weak. Tüm SmartPtr yapıcıları da aynı değerleri kabul eder. Sonuç olarak, her işaretçi örneği iki durumdan birinde bulunabilir:

  1. Güçlü referans: referans sayacı nesnenin içinde kapsüllenmiştir;
  2. Zayıf referans: referans sayacı nesnenin dışındadır.

Modlar arasında geçiş, çalışma zamanında ve herhangi bir anda gerçekleşebilir. Zayıf referans sayacı, nesneye en az bir zayıf referans olana kadar oluşturulmaz.

İşaretçimizin desteklediği özelliklerin tam listesi şöyledir:

  1. Güçlü referans saklama: referans sayımı yoluyla nesne ömrü yönetimi.
  2. Bir nesne için zayıf referans saklama.
  3. intrusive_ptr semantiği: aynı nesne için oluşturulan herhangi bir sayıda işaretçi tek bir referans sayacını paylaşacaktır.
  4. Dereferans etme ve ok operatörü (->): işaret edilen nesneye erişim için.
  5. Tam bir yapıcı ve atama operatörleri seti.
  6. İşaret edilen nesne ile referans sayılan nesnenin ayrılması (takma ad (aliasing) yapıcısı): müşterilerimizin kütüphaneleri belgelerle çalıştığından, genellikle bir belge öğesine işaret eden bir işaretçinin tüm belgeyi hayatta tutması gerekir.
  7. Tam bir tür dönüşümleri (casts) seti.
  8. Tam bir karşılaştırma işlemleri seti.
  9. İşaretçilerin atanması ve silinmesi: tamamlanmamış türler üzerinde çalışır.
  10. İşaretçi durumunu kontrol etmek ve değiştirmek için bir metot seti: takma ad modu, referans saklama modu, nesne referans sayısı vb.

SmartPtr sınıfı şablonludur (templated) ve sanal metotlar içermez. Referans sayacı depolamasını yöneten System::Object sınıfıyla sıkı sıkıya bağlıdır ve yalnızca onun türetilmiş sınıflarıyla çalışır.

Tipik işaretçi davranışından sapmalar vardır:

  1. Taşıma (taşıma yapıcısı, taşıma atama operatörü) tüm durumu değiştirmez; referans türünü (zayıf/güçlü) korur.
  2. Zayıf bir referans aracılığıyla bir nesneye erişim, kilitleme (geçici bir güçlü referans oluşturma) gerektirmez, çünkü ok operatörünün geçici bir nesne döndürdüğü bir yaklaşım, güçlü referanslar için performansı ciddi şekilde düşürür.

Eski kodla uyumluluğu sürdürmek için SharedPtr türü SmartPtr için bir takma ad (alias) haline geldi. WeakPtr sınıfı artık SmartPtr'dan miras alır, alan eklemez ve yalnızca her zaman zayıf referanslar oluşturmak için yapıcıları geçersiz kılar.

Konteynerler artık her zaman MyContainer<SmartPtr<MyClass>> semantiği ile taşınır ve saklanan referansların türü çalışma zamanında seçilir. STL veri yapılarına dayalı olarak manuel olarak yazılan konteynerler için (öncelikle System ad alanındaki konteynerler), varsayılan referans türü özel bir ayırıcı (custom allocator) kullanılarak ayarlanır, ancak yine de tek tek konteyner öğeleri için modun değiştirilmesine izin verilir. Çevrilen konteynerler için, referans saklama modunu değiştirmek için gerekli kod çevirici tarafından oluşturulur.

Bu çözümün dezavantajları öncelikle işaretçi oluşturma, kopyalama ve silme işlemleri sırasında performansın düşmesini içerir, çünkü olağan referans sayımına referans türünün zorunlu bir kontrolü eklenir. Spesifik rakamlar büyük ölçüde test yapısına bağlıdır. İşaretçi türünün değişmeyeceğinin garanti edildiği yerlerde daha optimize kod üretme konusunda tartışmalar halen devam etmektedir.

Çeviri İçin Kod Hazırlama

Taşıma yöntemimiz, referansların nerede zayıf olması gerektiğini işaretlemek için kaynak C# koduna manuel olarak nitelikler yerleştirmeyi gerektirir. Bu niteliklerin doğru yerleştirilmediği kod, çeviriden sonra bellek sızıntılarına ve bazı durumlarda başka hatalara neden olacaktır. Niteliklere sahip kod şuna benzer:

struct S {
    MyClass s; // Nesneye güçlü referans

    [CppWeakPtr]
    MyClass w; // Nesneye zayıf referans

    MyContainer<MyClass> s_s; // Güçlü referanslar içeren bir konteynere güçlü referans

    [CppWeakPtr]
    MyContainer<MyClass> w_s; // Güçlü referanslar içeren bir konteynere zayıf referans

    [CppWeakPtr(0)]
    MyContainer<MyClass> s_w; // Zayıf referanslar içeren bir konteynere güçlü referans

    [CppWeakPtr(1)]
    Dictionary<MyClass, MyClass> s_s_w; // Anahtarların güçlü referanslarla, değerlerin ise zayıf referanslarla saklandığı bir konteynere güçlü referans

    [CppWeakPtr, CppWeakPtr(0)]
    Dictionary<MyClass, MyClass> w_w_s; // Anahtarların zayıf referanslarla, değerlerin ise güçlü referanslarla saklandığı bir konteynere zayıf referans
}

Bazı durumlarda, SmartPtr sınıfının takma ad (aliasing) yapıcısını veya saklanan referans türünü ayarlayan metodunu manuel olarak çağırmak gerekir. Taşınan kodu düzenlemekten kaçınmaya çalışıyoruz, çünkü bu tür değişikliklerin her çevirici çalıştırıldıktan sonra yeniden uygulanması gerekir. Bunun yerine, bu tür kodları C# kaynağında tutmayı hedefliyoruz. Bunu başarmanın iki yolu vardır:

  1. C# kodunda hiçbir şey yapmayan bir hizmet metodu bildirebilir ve çeviri sırasında bunu, gerekli işlemi gerçekleştiren manuel olarak yazılmış bir eşdeğeriyle değiştirebiliriz:
class Service {
    public static void SetWeak<T>(T arg) {}
}
class Service {
public:
    template <typename T> static void SetWeak(SmartPtr<T> &arg)
    {
        arg.set_Mode(SmartPtrMode::Weak);
    }
};
  1. C# koduna özel olarak biçimlendirilmiş yorumlar yerleştirebiliriz, çevirici bunları C++ koduna dönüştürür:
class MyClass {
    private Dictionary<string, object> data;
    public void Add(string key, object value)
    {
        data.Add(key, value);
        //CPPCODE: if (key == u"Parent") data->data()[key].set_Mode(SmartPtrMode::Weak);
    }
}

Burada, System::Collections::Generic::Dictionary içindeki data() metodu, bu konteynerin temelindeki std::unordered_map'e bir referans döndürür.

İlgili Haberler

İlgili videolar

İlgili makaleler