Merhaba sevgili yazılımperver dostlarım bir başka haftalık C++ yazım ile birlikteyiz. Bu yazımda, eminim bir çoğunuzun ihtiyacını hissettiği C++ “serialization/deserialization” ihtiyacına yardımcı olabilecek alternatiflerden birine değineceğim: Cereal Kütüphanesi. Diğer alternatifler için https://github.com/thekvs/cpp-serializers sayfasına bakabilirsiniz.
Açıkçası, uzun bir süre önce boost kütüphanelerinin sunduğu “serialization” kabiliyetlerini kullanmıştım. Daha sonraları ise, json ve xml kütüphanelerini kullanarak, bu tarz ihtiyaçlarımı kendim, basit sınıflar yazarak giderdim.
Kişisel bir ihtiyaç için neler var diye baktığımda Cereal kütüphanesine denk geldim. Öncelikle ihtiyaçları netleştirme adına burada sıralayalım:
- Basit bir şekilde veri yapılarının içeriklerini .json (ve ileride .xml, belki binary) dosyası olarak kaydetmek (gerek konfigürasyon gerekse veri),
- Benzer şekilde bu tarz dosyaları okumak.
Şu an için ağ üzerinden paylaşım için bu tarz bir ihtiyacım olmadığı için çok fazla bir kısıtım yok. Bu tarz ihtiyacı olan arkadaşlar Google’ın ProtoBuf’ına göz atabilirler. Özellikle, kapsamlı veri paylaşımları için bu alternatif bana da daha çekici geliyor. Buna da bir yazımda değineceğim.
Gelelim kullanıma. Öncelikle C++ 11 destekli bir derleyiciye ihtiyacınız bulunmakta. Öncelikle https://github.com/USCiLab/cereal adresinden kütüphaneyi indirmeniz gerekmekte. Kütüphane başlık dosyalarından oluşmakta, bu sebeple ilgili başlık dosyalarını, kaynak kod içerisinden eklemeniz yeterli (cereal-1.3.2\include dizininde bulunuyor, benim indirdiğim sürüm için).
Şimdi gelelim örnek olarak saklamak istediğimiz veri yapısına bir göz atalım:
1 2 3 4 5 6 7 8 9 10 |
struct WindowParameter { std::string Title{"Default Window"}; uint32_t Width{640}; uint32_t Height{480}; uint32_t Top{0}; uint32_t Left{0}; Color ClearColor{180, 50, 79}; bool IsFullScreen{false}; bool IsVSYNCEnabled{ false }; }; |
sanırım isminden ne sakladığını az çok anlamışsınızdır. Ayrıca bu veri yapısı içerisinde, bir de renk sınıfımız var o da yaklaşık aşağıdaki gibi bir sınıf:
1 2 3 4 5 6 7 |
class Color { ... uint8_t R; uint8_t G; uint8_t B; uint8_t A; }; |
Şimdi bunu json formatında yazmak ve okumak için ne yapacağız ona bakacağız. Öncelikli olarak, hedef veri formatına göre aşağıdaki başlık dosyalarını eklemeniz gerekiyor:
#include <cereal/archives/binary.hpp>
#include <cereal/archives/portable_binary.hpp>
#include <cereal/archives/xml.hpp>
#include <cereal/archives/json.hpp>
Daha sonra, serialization/deserialization fonksiyonlarını nasıl ve nerede yazacağınız geliyor. Cereal, bunun için bir kaç yöntem sunmakta. Detaylarına https://uscilab.github.io/cereal/serialization_functions.html sayfasında ulaşacağınız yöntemler temelde aşağıdaki yaklaşımlara dayanmakta:
- Sınıfa tek bir metot ekleyerek her iki işi (“serialization/deserialization”) yapmak,
- Sınıfa her iki işlem için ayrı ayrı metotlar eklemek,
- Sınıf dışında tek bir metot eklemek,
- Sınıfa dışında iki işlem için ayrı ayrı metotlar eklemek,
Ben burada, 3. yaklaşımı tercih ettim, orjinal sınıf içerisine müdahale etmeden, en kısa şekilde çözmek için. Fakat, “serialization/deserialization” işlevini ayro bir sınıf içerisine ekledim. Öncelikle, Color sınıfı için nasıl bir kod parçasına ihtiyacımız var ona bakalım:
1 2 3 4 5 6 7 8 9 |
#include <cereal/archives/json.hpp> template<class Archive> void serialize(Archive& archive, Color& m) { archive(cereal::make_nvp("Red", m.R), cereal::make_nvp("Green", m.G), cereal::make_nvp("Blue", m.B), cereal::make_nvp("Alpha", m.A)); } |
cereal::make_nvp( name, value ) ibaresi zorunlu değil fakat bunu eklerseniz, Json içerisindeki isimlendirmeleri kontrol edebilirsiniz. Yoksa varsayılan, fakat pek kullanıcı dostu olmayan bir isimlendirme ile karşılaşabilirsiniz (birazdan örnek vereceğim).
Şimdi bir de, WindowParameter sınıfı için hazırlayacağımız koda bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <cereal/archives/json.hpp> template<class Archive> void serialize(Archive& archive, WindowParameter& m) { archive(cereal::make_nvp("Title", m.Title), cereal::make_nvp("Width", m.Width), cereal::make_nvp("Height", m.Height), cereal::make_nvp("Top", m.Top), cereal::make_nvp("Left", m.Left), cereal::make_nvp("ClearColor", m.ClearColor), cereal::make_nvp("IsFullScreen", m.IsFullScreen), cereal::make_nvp("IsVsyncEnabled", m.IsVSYNCEnabled)); } |
Yukarıdaki satırlar dışında, “serialization/deserialization” için bir satır kod yazmanıza gerek yok. Temel veri tipleri için ilave bir kod parçası eklemenize ihtiyaç yok, diğerleri için, benzer şekilde serialize fonksiyonunu tanımlamanız gerekmekte.
Son olarak gelelim asıl sınıfa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
template <class T> class JsonDataSerializer{ public: std::optional<T> Deserialize(std::string_view fileName) { std::ifstream is(fileName.data()); if (is.is_open()) { try { T parsedData; cereal::JSONInputArchive archive_in(is); archive_in(parsedData); return parsedData; } catch (cereal::RapidJSONException&) { return std::nullopt; } } else { return std::nullopt; } } bool Serialize(std::string_view fileName, const T& data, std::string_view jsonObjName = "Object") { try { std::ofstream os(fileName.data()); cereal::JSONOutputArchive archive_out(os); archive_out(cereal::make_nvp(jsonObjName.data(), data)); return true; } catch (cereal::RapidJSONException&) { return false; } } }; |
Bu sınıf ile “serialization/deserialization” işlevi kontrol edebilirsiniz. Ayrıca, .xml vs de benzer bir sınıf ile kotarılabilir. Son olarak kullanıma bir bakalım:
1 2 3 4 5 6 7 |
WindowParameter param, param2; JsonDataParser<WindowParameter> wpParser; std::cout << "Serialize op result: " << wpParser.Serialize("data.json", param) << "\n"; auto opResult = wpParser.Deserialize("data.json"); std::cout << "Deserialize op result: " << opResult.has_value() << "\n"; |
Pastanın, kirazını sona bıraktık 🙂 Oluşan json dosyası nasıl oluyor ona bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "Object": { "Title": "Default Window", "Width": 640, "Height": 480, "Top": 0, "Left": 0, "ClearColor": { "Red": 180, "Green": 50, "Blue": 79, "Alpha": 255 }, "IsFullScreen": false, "IsVsyncEnabled": false } } |
Yukarıda belirttiğim cereal::make_nvp’yi kullanmazsanız, aşağıdaki gibi bir çıktı elde edersiniz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "Object": { "value0": "Default Window", "value1": 640, "value2": 480, "value3": 0, "value4": 0, "value5": { "value0": 180, "value1": 50, "value2": 79, "value3": 255 }, "value6": false, "value7": false } } |
Yukarıda bahsettiğim hususlar yanında, tiplere sürüm ekleme, türetilen sınıflar için de kabiliyetler mevcut, bunlar için de kütüphanenin sayfasına göz atabilirsiniz.
Bir sonraki yazımda görüşmek dileğiyle, bol kodlu günler diliyorum.