التسلسل في C ++

سوف تركز هذه المقالة على أتمتة عملية التسلسل في C ++. في البداية ، سننظر في الآليات الأساسية التي تسهل قراءة / كتابة البيانات على تدفقات الإدخال / الإخراج ، وبعد ذلك سيتم تقديم وصف لنظام توليد الشفرة المستند إلى libclang. يوجد رابط للمستودع به نسخة تجريبية من المكتبة في نهاية المقال.

في ruSO ، تنشأ أسئلة بشكل دوري فيما يتعلق بتسلسل البيانات في C ++ ، وأحيانًا تكون هذه الأسئلة عامة بطبيعتها ، عندما لا تعرف TC أساسًا من أين تبدأ ، وأحيانًا تكون هذه أسئلة تصف مشكلة محددة. الغرض من هذه المقالة هو تلخيص إحدى الطرق الممكنة لتنفيذ التسلسل في C ++ ، مما سيتيح لك اتباع خطوات إنشاء نظام من الخطوات الأولية إلى بعض الاستنتاجات المنطقية ، عندما يمكن بالفعل استخدام هذا النظام في الممارسة.

1. المعلومات الأولية


ستستخدم هذه المقالة تنسيق بيانات ثنائي ، يتم تحديد هيكله استنادًا إلى أنواع الكائنات القابلة للتسلسل. هذا النهج يحفظنا من استخدام مكتبات الطرف الثالث ، ويقصرنا فقط على الأدوات التي توفرها مكتبة C ++ القياسية.

نظرًا لأن عملية التسلسل تتكون من تحويل حالة كائن إلى دفق من وحدات البايت ، والذي من الواضح أنه يجب أن يكون مصحوبًا بعمليات كتابة ، سيتم استخدام الأخير بدلاً من مصطلح "التسلسل" عند وصف تفاصيل ذات مستوى منخفض. وبالمثل للقراءة / إلغاء التسلسل.

لتقليل حجم المقالة ، سيتم تقديم أمثلة فقط لتسلسل الكائنات (باستثناء الحالات التي تحتوي فيها عملية إلغاء التسلسل على بعض التفاصيل الجديرة بالذكر). يمكن العثور على الكود الكامل في المستودع أعلاه.

2. أنواع المدعومة


بادئ ذي بدء ، تجدر الإشارة إلى الأنواع التي نخطط لدعمها - وهذا يعتمد بشكل مباشر على كيفية تنفيذ المكتبة.

على سبيل المثال ، إذا كان الاختيار مقصورًا على الأنواع الأساسية لـ C ++ ، فستكون قالب دالة (وهي مجموعة من الوظائف للعمل بقيم أنواع الأعداد الصحيحة) وتخصصاتها الصريحة كافية. القالب الأساسي (يستخدم للأنواع std :: int32_t ، std :: uint16_t ، إلخ):

template<typename T> auto write(std::ostream& os, T value) -> std::size_t { const auto pos = os.tellp(); os.write(reinterpret_cast<const char*>(&value), sizeof(value)); return static_cast<std::size_t>(os.tellp() - pos); } 

ملاحظة : إذا كان من المخطط نقل البيانات التي تم الحصول عليها أثناء التسلسل بين الأجهزة ذات أوامر البايت المختلفة ، فمن الضروري ، على سبيل المثال ، تحويل قيمة من ترتيب البايت المحلي إلى بايت شبكة ، ثم إجراء العملية العكسية على الجهاز البعيد ، لذلك ستكون هناك حاجة لإجراء تغييرات لوظيفة الكتابة البيانات إلى دفق الإخراج ، ووظيفة القراءة من دفق الإدخال.

التخصص المنطقي:

 constexpr auto t_value = static_cast<std::uint8_t>('T'); constexpr auto f_value = static_cast<std::uint8_t>('F'); template<> auto write(std::ostream& os, bool value) -> std::size_t { const auto pos = os.tellp(); const auto tmp = (value) ? t_value : f_value; os.write(reinterpret_cast<const char*>(&tmp), sizeof(tmp)); return static_cast<std::size_t>(os.tellp() - pos); } 

تعرّف هذه الطريقة القاعدة التالية: إذا أمكن تمثيل قيمة من النوع T كسلسلة من بايتات من حجم sizeof (T) ، يمكن استخدام تعريف القالب الأساسي لذلك ، وإلا ، فمن الضروري تحديد التخصص. يمكن إملاء هذا المتطلب من خلال ميزات تمثيل كائن من النوع T في الذاكرة.

ضع في اعتبارك حاوية std :: string: من الواضح أننا لا نستطيع أن نأخذ عنوان كائن من النوع المحدد ، وننقله إلى مؤشر لتوجيهه وكتابته إلى قطار الإخراج - وهذا يعني أننا بحاجة إلى التخصص:

 template<> auto write(std::ostream& os, const std::string& value) -> std::size_t { const auto pos = os.tellp(); const auto len = static_cast<std::uint32_t>(value.size()); os.write(reinterpret_cast<const char*>(&len), sizeof(len)); if (len > 0) os.write(value.data(), len); return static_cast<std::size_t>(os.tellp() - pos); } 

نقطتان مهمتان يجب ذكرهما هنا:

  1. ليس فقط محتويات السلسلة مكتوبة على دفق الإخراج ، ولكن أيضًا حجمها.
  2. Cast std :: string :: size_type لكتابة std :: uint32_t. في هذه الحالة ، يجدر الانتباه لا إلى حجم النوع المستهدف ، ولكن إلى حقيقة أنه ذو طول ثابت. سيسمح هذا التخفيض بتجنب المشكلات في الحالة ، على سبيل المثال ، إذا تم نقل البيانات عبر شبكة بين الأجهزة ذات أحجام مختلفة من الكلمات الجهاز.

لذلك ، اكتشفنا أن قيم الأنواع الأساسية (وحتى الكائنات من النوع std :: string) يمكن كتابتها إلى دفق الإخراج باستخدام قالب دالة الكتابة . دعونا الآن نحلل التغييرات التي نحتاج إلى إجرائها إذا أردنا إضافة حاويات إلى قائمة الأنواع المدعومة. لدينا خيار واحد فقط للحمل الزائد - استخدم المعلمة T كنوع من عناصر الحاوية. وإذا كان الأمر في حالة الأمراض المنقولة جنسيا :: vector سيعمل هذا:

 template<typename T> auto write(std::ostream& os, const std::vector<T>& value) -> std::size_t { const auto pos = os.tellp(); const auto len = static_cast<std::uint16_t>(value.size()); os.write(reinterpret_cast<const char*>(&len), sizeof(len)); auto size = static_cast<std::size_t>(os.tellp() - pos); if (len > 0) { std::for_each(value.cbegin(), value.cend(), [&](const auto& e) { size += ::write(os, e); }); } return size; } 

، ثم باستخدام std: map - لا ، لأن قالب std :: map يتطلب معلمتين على الأقل - نوع المفتاح ونوع القيمة. وبالتالي ، في هذه المرحلة ، لم يعد بإمكاننا استخدام قالب الوظيفة - نحن بحاجة إلى حل عالمي أكثر. قبل أن نتعرف على كيفية إضافة دعم الحاوية ، دعنا نتذكر أنه لا يزال لدينا فئات مخصصة. من الواضح ، حتى باستخدام الحل الحالي ، لن يكون من الحكمة تحميل وظيفة الكتابة لكل فئة تتطلب إجراء تسلسل. في أفضل الأحوال ، نود أن يكون لدينا تخصص واحد لنمط الكتابة الذي يعمل مع أنواع البيانات المخصصة. لكن من الضروري لهذا أن يكون للفئات القدرة على التحكم في التسلسل بشكل مستقل ، على التوالي ، يجب أن يكون لديهم واجهة تسمح للمستخدم بتسلسل كائنات هذه الفئة وإلغاء تسلسلها. كما اتضح فيما بعد ، ستعمل هذه الواجهة بمثابة "قاسم مشترك" لقالب الكتابة عند العمل مع الفئات المخصصة. دعونا نحدده.

 class ISerializable { protected: ~ISerializable() = default; public: virtual auto serialize(std::ostream& os) const -> std::size_t = 0; virtual auto deserialize(std::istream& is) -> std::size_t = 0; virtual auto serialized_size() const noexcept -> std::size_t = 0; }; 

أي فئة ترث من ISerializable توافق على:

  1. تجاوز التسلسل - كتابة الحالة (أعضاء البيانات) إلى دفق الإخراج.
  2. Override deserialize - قراءة الحالة (تهيئة أعضاء البيانات) من دفق الإدخال.
  3. Override serialized_size - يحسب حجم البيانات المتسلسلة للحالة الحالية للكائن.

لذلك ، بالعودة إلى قالب وظيفة الكتابة : بشكل عام ، يمكننا تنفيذ التخصص لفئة ISerializable ، لكن لا يمكننا استخدامه ، نلقي نظرة:

 template<> auto write(std::ostream& os, const ISerializable& value) -> std::size_t { return value.serialize(os); } 

في كل مرة ، يتعين علينا أن نلقي نوع الوريث على ISerializable للاستفادة من هذا التخصص. اسمحوا لي أن أذكرك بأننا في البداية حددنا هدفنا لتبسيط كتابة التعليمات البرمجية المتعلقة بالتسلسل ، وليس العكس ، لتعقيد ذلك. لذلك ، إذا كانت الأنواع التي تدعمها مكتبتنا لا تقتصر على الأنواع الأساسية ، فينبغي أن نبحث عن حل آخر.

3. stream_writer


لم يكن استخدام قوالب الوظائف لتنفيذ واجهة عالمية لكتابة البيانات إلى دفق حلاً مناسبًا تمامًا. الخيار التالي الذي يجب التحقق منه هو قالب الفصل. سنتبع نفس المنهجية المستخدمة في قالب الوظيفة - سيتم استخدام القالب الأساسي افتراضيًا ، وستتم إضافة تخصصات واضحة لدعم الأنواع اللازمة.

بالإضافة إلى ذلك ، يجب أن نأخذ بعين الاعتبار كل ما سبق حول ISerializable - من الواضح أننا لن نتمكن من حل المشكلة مع العديد من الفئات اللاحقة دون اللجوء إلى type_traits: بدءًا من C ++ 11 ، ظهر القالب std :: enable_if في المكتبة القياسية ، والذي يسمح بتجاهل فئات القوالب عند شروط معينة أثناء التجميع - وهذا هو بالضبط ما سنستفيد منه.

قالب فئة Stream_writer :

 template<typename T, typename U = void> class stream_writer { public: static auto write(std::ostream& os, const T& value) -> std::size_t; }; 

تعريف طريقة الكتابة :

 template<typename T, typename U> auto stream_writer<T, U>::write(std::ostream& os, const T& value) -> std::size_t { const auto pos = os.tellp(); os.write(reinterpret_cast<const char*>(&value), sizeof(value)); return static_cast<std::size_t>(os.tellp() - pos); } 

التخصص في ISerializable سيكون على النحو التالي:

 template<typename T> class stream_writer<T, only_if_serializable<T>> : public stream_io<T> { public: static auto write(std::ostream& os, const T& value) -> std::size_t; }; 

حيث only_if_serializable هو نوع المساعد:

 template<typename T> using only_if_serializable = std::enable_if_t<std::is_base_of_v<ISerializable, T>>; 

وبالتالي ، إذا كان النوع T فئة مشتقة من ISerializable ، فسيتم اعتبار هذا التخصص مرشحًا لإنشاء مثيل ، على التوالي ، إذا لم يكن النوع T في نفس التسلسل الهرمي للفئة ، فسيتم استبعاده من المرشحين المحتملين.

سيكون من العدل طرح السؤال التالي هنا: كيف سيعمل هذا؟ بعد كل شيء ، سيكون للقالب الأساسي نفس قيم المعلمات النموذجية مثل تخصصه - <T ، void>. لماذا يتم إعطاء الأفضلية للتخصص ، وهل سيكون؟ الجواب: سوف يكون ، لأن هذا السلوك يحدده المعيار ( المصدر ):

(1.1) إذا تم العثور على تخصص مطابقة واحد بالضبط ، يتم إنشاء إنشاء مثيل من هذا التخصص

سيبدو التخصص الخاص بـ std :: string الآن كما يلي:

 template<typename T> class stream_writer<T, only_if_string<T>> { public: static auto write(std::ostream& os, const T& value) -> std::size_t; }; template<typename T> auto stream_writer<T, only_if_string<T>>::write(std::ostream& os, const T& value) -> std::size_t { const auto pos = os.tellp(); const auto len = static_cast<std::uint32_t>(value.size()); os.write(reinterpret_cast<const char*>(&len), sizeof(len)); if (len > 0) os.write(value.data(), len); return static_cast<std::size_t>(os.tellp() - pos); } 

حيث يتم إعلان only_if_string كـ:

 template<typename T> using only_if_string = std::enable_if_t<std::is_same_v<T, std::string>>; 

لقد حان الوقت للعودة إلى الحاويات. في هذه الحالة ، يمكننا استخدام نوع الحاوية المعلمة مع نوع من U ، أو <U ، V> ، كما في حالة std :: map ، مباشرة كقيمة المعلمة T لقالب فئة stream_writer . وبالتالي ، لا شيء يتغير في الواجهة في واجهتنا - وهذا هو ما نهدف إليه. ومع ذلك ، فإن السؤال الذي يطرح نفسه ، ما ينبغي أن يكون المعلمة الثانية من القالب للفئة stream_writer بحيث يعمل كل شيء بشكل صحيح؟ هذا في الفصل التالي.

4. المفاهيم


أولاً ، سأقدم وصفًا موجزًا ​​للمفاهيم المستخدمة ، وعندها فقط سأعرض أمثلة محدثة.

 template<typename T> concept String = std::is_same_v<T, std::string>; 

بصراحة ، تم تعريف هذا المفهوم للاحتيال ، والذي سنراه في السطر التالي:

 template<typename T> concept Container = !String<T> && requires (T a) { typename T::value_type; typename T::reference; typename T::const_reference; typename T::iterator; typename T::const_iterator; typename T::size_type; { a.begin() } -> typename T::iterator; { a.end() } -> typename T::iterator; { a.cbegin() } -> typename T::const_iterator; { a.cend() } -> typename T::const_iterator; { a.clear() } -> void; }; 

تحتوي الحاوية على المتطلبات التي "نقوم بها" للنوع للتأكد حقًا من أنه أحد أنواع الحاويات. هذا هو بالضبط مجموعة المتطلبات التي سنحتاجها عند تنفيذ stream_writer ، يحتوي المعيار على الكثير من المتطلبات ، بالطبع.

 template<typename T> concept SequenceContainer = Container<T> && requires (T a, typename T::size_type count) { { a.resize(count) } -> void; }; 

مفهوم للحاويات المتسلسلة: الأمراض المنقولة جنسيا :: ناقل ، الأمراض المنقولة جنسيا :: قائمة ، الخ

 template<typename T> concept AssociativeContainer = Container<T> && requires (T a) { typename T::key_type; }; 

مفهوم الحاويات الترابطية: std :: map ، std :: set ، std :: unordered_map ، إلخ

الآن ، لتحديد التخصص للحاويات المتتالية ، كل ما تبقى علينا هو فرض قيود على النوع T:

 template<typename T> requires SequenceContainer<T> class stream_writer<T, void> { public: static auto write(std::ostream& os, const T& value) -> std::size_t; }; template<typename T> requires SequenceContainer<T> auto stream_writer<T, void>::write(std::ostream& os, const T& value) -> std::size_t { const auto pos = os.tellp(); // to support std::forward_list we have to use std::distance() const auto len = static_cast<std::uint16_t>( std::distance(value.cbegin(), value.cend())); os.write(reinterpret_cast<const char*>(&len), sizeof(len)); auto size = static_cast<std::size_t>(os.tellp() - pos); if (len > 0) { using value_t = typename stream_writer::value_type; std::for_each(value.cbegin(), value.cend(), [&](const auto& item) { size += stream_writer<value_t>::write(os, item); }); } return size; } 

الحاويات المدعومة:

  • الأمراض المنقولة جنسيا :: ناقلات
  • الأمراض المنقولة جنسيا :: deque
  • الأمراض المنقولة جنسيا :: قائمة
  • الأمراض المنقولة جنسيا :: forward_list

وبالمثل بالنسبة للحاويات الترابطية:

 template<typename T> requires AssociativeContainer<T> class stream_writer<T, void> : public stream_io<T> { public: static auto write(std::ostream& os, const T& value) -> std::size_t; }; template<typename T> requires AssociativeContainer<T> auto stream_writer<T, void>::write(std::ostream& os, const T& value) -> std::size_t { const auto pos = os.tellp(); const auto len = static_cast<typename stream_writer::size_type>(value.size()); os.write(reinterpret_cast<const char*>(&len), sizeof(len)); auto size = static_cast<std::size_t>(os.tellp() - pos); if (len > 0) { using value_t = typename stream_writer::value_type; std::for_each(value.cbegin(), value.cend(), [&](const auto& item) { size += stream_writer<value_t>::write(os, item); }); } return size; } 

الحاويات المدعومة:

  • الأمراض المنقولة جنسيا :: خريطة
  • الأمراض المنقولة جنسيا :: unordered_map
  • الأمراض المنقولة جنسيا :: مجموعة
  • الأمراض المنقولة جنسيا :: unordered_set

في حالة الخريطة ، هناك فارق بسيط صغير ، يتعلق بتنفيذ stream_reader . value_type لـ std :: map <K، T> هي std :: pair <const K، T> ، على التوالي ، عندما نحاول إرسال مؤشر لتقييد K إلى مؤشر إلى char عند القراءة من دفق الإدخال ، نحصل على خطأ في الترجمة. يمكننا حل هذه المشكلة على النحو التالي: نحن نعلم أنه بالنسبة للحاويات الترابطية ، تعد value_type من النوع الواحد K أو std :: pair <const K، V> ، ثم نتمكن من كتابة فئات المساعد الصغيرة للقالب والتي سيتم تحديدها بواسطة value_type وداخلها تحديد النوع الذي نحتاجه.

بالنسبة إلى std :: set ، يبقى كل شيء كما هو دون تغيير:

 template<typename U, typename V = void> struct converter { using type = U; }; 

بالنسبة إلى std :: map - remove const:

 template<typename U> struct converter<U, only_if_pair<U>> { using type = std::pair<std::remove_const_t<typename U::first_type>, typename U::second_type>; }; 

تعريف القراءة للحاويات النقابية:

 template<typename T> requires AssociativeContainer<T> auto stream_reader<T, void>::read(std::istream& is, T& value) -> std::size_t { const auto pos = is.tellg(); typename stream_reader::size_type len = 0; is.read(reinterpret_cast<char*>(&len), sizeof(len)); auto size = static_cast<std::size_t>(is.tellg() - pos); if (len > 0) { for (auto i = 0U; i < len; ++i) { using value_t = typename converter<typename stream_reader::value_type>::type; value_t v {}; size += stream_reader<value_t>::read(is, v); value.insert(std::move(v)); } } return size; } 


5. وظائف مساعدة


النظر في مثال:

 class User : public ISerializable { public: User(std::string_view username, std::string_view password) : m_username(username) , m_password(password) {} SERIALIZABLE_INTERFACE protected: std::string m_username {}; std::string m_password {}; }; 

يجب أن يبدو تعريف طريقة التسلسل (std :: ostream &) لهذه الفئة كما يلي:

 auto User::serialize(std::ostream& os) const -> std::size_t { auto size = 0U; size += stream_writer<std::string>::write(os, m_username); size += stream_writer<std::string>::write(os, m_password); return size; } 

ومع ذلك ، يجب أن تعترف أنه من غير المريح الإشارة في كل مرة إلى نوع الكائن الذي يتم كتابته إلى دفق الإخراج. نكتب وظيفة مساعدة من شأنها أن نستنتج تلقائيًا النوع T:

 template<typename T> auto write(std::ostream& os, const T& value) -> std::size_t { return stream_writer<T>::write(os, value); } 

الآن التعريف كالتالي:

 auto User::serialize(std::ostream& os) const -> std::size_t { auto size = 0U; size += ::write(os, m_username); size += ::write(os, m_password); return size; } 

سيتطلب الفصل الأخير بعض وظائف المساعد الإضافية:

 template<typename T> auto write_recursive(std::ostream& os, const T& value) -> std::size_t { return ::write(os, value); } template<typename T, typename... Ts> auto write_recursive(std::ostream& os, const T& value, const Ts&... values) { auto size = write_recursive(os, value); return size + write_recursive(os, values...); } template<typename... Ts> auto write_all(std::ostream& os, const Ts&... values) -> std::size_t { return write_recursive(os, values...); } 

تتيح لك وظيفة write_all سرد جميع الكائنات المراد تسلسلها في وقت واحد ، بينما تضمن كتابة_الترتيب الصحيح ترتيب الكتابة إلى دفق الإخراج. إذا تم تحديد ترتيب العمليات الحسابية للتعبيرات الطية (شريطة أن نستخدم عامل التشغيل الثنائي +) ، فيمكننا استخدامها. على وجه الخصوص ، في الدالة size_of_all (لم يتم ذكرها سابقًا ، يتم استخدامها لحساب حجم البيانات المتسلسلة) ، وهي عبارة عن تعبيرات قابلة للطي تستخدم بسبب عدم وجود عمليات الإدخال والإخراج.

6. كود الجيل


يتم استخدام libclang - C API لـ clang لإنشاء الشفرة. يمكن وصف هذه المهمة رفيعة المستوى على النحو التالي: نحن بحاجة إلى التنقل بشكل متكرر حول الدليل مع الكود المصدر ، والتحقق من جميع ملفات الرأس للفئات التي تحمل سمة خاصة ، وإذا كان هناك واحد ، تحقق من أعضاء البيانات لنفس السمة وتجميع السلسلة من أسماء أعضاء البيانات المدرجة مع فاصلة. كل ما تبقى بالنسبة لنا هو كتابة قوالب تعريف لوظائف الفئة ISerializable (حيث يمكننا فقط وضع عدد أعضاء البيانات الضروريين).

مثال على فئة سيتم إنشاء الكود لها:

 class __attribute__((annotate("serializable"))) User : public ISerializable { public: User(std::string_view username, std::string_view password) : m_username(username) , m_password(password) {} User() = default; virtual ~User() = default; SERIALIZABLE_INTERFACE protected: __attribute__((annotate("serializable"))) std::string m_username {}; __attribute__((annotate("serializable"))) std::string m_password {}; }; 

تتم كتابة السمات بأسلوب غنو لأن libclang يرفض التعرف على تنسيق السمة من C ++ 20 ، ولا يدعم السمات غير المشروحة أيضًا. المصدر دليل السفر:

 for (const auto& file : fs::recursive_directory_iterator(argv[1])) { if (file.is_regular_file() && file.path().extension() == ".hpp") { processTranslationUnit(file, dst); } } 

تعريف processTranslationUnit وظيفة:

 auto processTranslationUnit(const fs::path& path, const fs::path& targetDir) -> void { const auto pathname = path.string(); arg::Context context { false, false }; auto translationUnit = arg::TranslationUnit::parse(context, pathname.c_str(), CXTranslationUnit_None); arg::ClassExtractor extractor; extractor.extract(translationUnit.cursor()); const auto& classes = extractor.classes(); for (const auto& [name, c] : classes) { SerializableDefGenerator::processClass(c, path, targetDir.string()); } } 

في هذه الوظيفة ، فقط ClassExtractor هو الذي يهمنا - كل شيء آخر ضروري لتشكيل AST. تعريف دالة الاستخراج كالتالي:

   void ClassExtractor::extract(const CXCursor& cursor) { clang_visitChildren(cursor, [](CXCursor c, CXCursor, CXClientData data) { if (clang_getCursorKind(c) == CXCursorKind::CXCursor_ClassDecl) { /*   */ /* -    - */ /* -     */ } return CXChildVisit_Continue; } , this); } 

هنا نرى بالفعل مباشرة وظائف C API ل clang. لقد تركنا عن عمد فقط الكود المطلوب لفهم كيفية استخدام libclang. كل ما تبقى وراء الكواليس لا يحتوي على معلومات مهمة - إنه مجرد تسجيل لأسماء الفصول وأعضاء البيانات وما إلى ذلك. يمكن العثور على كود أكثر تفصيلا في المستودع.

وأخيرًا ، في دالة processClass ، يتم التحقق من وجود سمات التسلسل لكل فئة تم العثور عليها ، وإذا كان هناك واحد ، يتم إنشاء ملف مع تعريف الوظائف الضرورية. يوفر المستودع أمثلة محددة: من أين يمكن الحصول على اسم / أسماء مساحة الاسم (يتم تخزين هذه المعلومات مباشرة في فئة الفصل الدراسي) والمسار إلى ملف الرأس.

بالنسبة للمهمة المذكورة أعلاه ، يتم استخدام مكتبة Argentum ، والتي ، للأسف ، لا أوصيك باستخدامها - لقد بدأت تطويرها لأغراض أخرى ، ولكن بسبب هذه المهمة كنت بحاجة فقط إلى الوظيفة التي تم تنفيذها هناك ، وكنت كسولًا ، لم أعد كتابة الرمز ، لكنني قمت بنشره على Bintray ووصلته بملف CMake من خلال مدير مجموعة Conan. كل ما توفره هذه المكتبة عبارة عن غلاف بسيط عبر واجهة برمجة تطبيقات clang C للفئات وأعضاء البيانات.

وملاحظة صغيرة أخرى - لا أقدم مكتبة جاهزة ، بل أقول فقط كيفية كتابتها.

UPD0 : يمكن استخدام cppast بدلاً من libclang . شكرا ل masterspline على الرابط المقدم.

1. github.com/isnullxbh/dsl
2. github.com/isnullxbh/Argentum

Source: https://habr.com/ru/post/ar479462/


All Articles