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

مقدمة قصيرة
أنا نفسي عثرت على ChaiScript عن طريق الصدفة عندما شاهدت
واحدة من محاضرات جايسون تيرنر ، واحدة من المبدعين في اللغة. لقد اهتمت بي ، وفي تلك اللحظة عندما كان من الضروري اختيار لغة برمجة في المشروع ، قررت - لماذا لا أجرب ChaiScript؟ النتيجة فاجأتني بسرور (ستتم كتابة تجربتي الشخصية بالقرب من نهاية المقال) ، ومع ذلك ، بغض النظر عن مدى غرابة الأمر ، لم يكن هناك مقال واحد في المحور الذي ذكر هذه اللغة بطريقة أو بأخرى ، وقررت ذلك سيكون من الجميل أن تكتب عنه. بالطبع ، تحتوي اللغة على
وثائق وموقع
رسمي ، ولكن لن يقرأها الجميع من الملاحظات ، ويكون تنسيق المقال أقرب إلى العديد (بما في ذلك أنا).
أولاً ، سنتحدث عن بناء الجملة للغة وجميع ميزاتها ، ثم حول كيفية تنفيذها في مشروع C ++ الخاص بك ، وفي النهاية سأتحدث قليلاً عن تجربتي. إذا كان جزء منك غير مهتم ، أو كنت ترغب في قراءة المقال بترتيب مختلف ، فيمكنك استخدام جدول المحتويات:
بناء اللغة
يشبه ChaiScript C ++ و JS في بناءه. بادئ ذي بدء ، فإنه ، مثل الغالبية العظمى من لغات البرمجة النصية ، يتم كتابته ديناميكيًا ، على عكس جافا سكريبت ، فإنه يحتوي على كتابة صارمة (رقم
1 + "2"
). هناك أيضًا أداة تجميع مجمعة للقمامة مضمّنة ، واللغة قابلة للتفسير تمامًا ، مما يسمح لك بتنفيذ التعليمات البرمجية سطرًا تلو الآخر ، دون تجميع في الرمز البريدي. إنه يدعم الاستثناءات (علاوة على ذلك ، المفصل ، مما يسمح لك بالقبض عليهم داخل النص وفي C ++) ، وظائف lambda ، التحميل الزائد للمشغل. ليست حساسة للمسافات ، مما يتيح لك الكتابة كسطر مفرد من خلال فاصلة منقوطة ، أو بأسلوب python ، تفصل بين التعبيرات بسطر جديد.
أنواع بدائية
يقوم ChaiScript افتراضيًا بتخزين متغيرات عدد صحيح مثل int ، وواقعية مزدوجة ، وسلاسل مع std :: string. ويتم ذلك في المقام الأول من أجل ضمان التوافق مع رمز الاتصال. تحتوي اللغة على لاحقات للأرقام ، حتى نتمكن من الإشارة بوضوح إلى نوع المتغير لدينا:
var myInt = 1
لا يعمل تغيير نوع المتغيرات فقط ، على الأرجح ستحتاج إلى تحديد عامل التشغيل الخاص بك `=` لهذه الأنواع ، وإلا فإنك تخاطر إما بإلقاء استثناء (سنتحدث عن هذا لاحقًا) أو ضحية التقريب ، مثل هذا:
var integer = 3 integer = 5.433 print(integer)
ومع ذلك ، يمكنك التصريح عن متغير دون تخصيص قيمة له ، وفي هذه الحالة سوف يحتوي على نوع غير معرف حتى يتم تعيين قيمة له.
حاويات مضمنة
تحتوي اللغة على حاويتين - Vector و Map. إنهم يعملون بشكل مشابه لنظرائهم في C ++ (std :: vector و std :: map ، على التوالي) ، لكنهم لا يحتاجون إلى نوع ، لأنه يمكنهم تخزين أي. يمكن إجراء الفهرسة كالمعتاد باستخدام ints ، لكن الخريطة تتطلب مفتاحًا بسلسلة. أضاف المؤلفون أيضًا أنه مستوحى بشكل واضح من بيثون ، القدرة على إعلان الحاويات بسرعة في الكود باستخدام الصيغة التالية:
var v = [ 1, 2, 3u, 4ll, "16", `+` ]
تكرّر هاتان الفئتان بشكل كامل تقريبًا نظرائهما في C ++ ، باستثناء التكرارات ، لأنه بدلاً من ذلك توجد فصول خاصة Range و Const_Range. بالمناسبة ، يتم تمرير جميع الحاويات بالرجوع إليها حتى لو كنت تستخدم الواجب خلال = ، وهو أمر غريب جدًا بالنسبة لي ، لأنه بالنسبة لجميع الأنواع الأخرى ، يحدث النسخ حسب القيمة.
البناء الشرطي
تقريبًا كافة وصفات الشروط والدورات يمكن وصفها في رمز مثال واحد حرفيًا:
var a = 5 var b = -1
أعتقد أن الأشخاص المطلعين على C ++ لم يعثروا على أي شيء جديد. هذا ليس مفاجئًا ، لأن ChaiScript يتم وضعه كلغة سهلة "للتعلم" للتعلم ، وبالتالي يستعير جميع التصميمات الكلاسيكية المعروفة. قرر المؤلفون تسليط الضوء حتى على كلمتين رئيسيتين لإعلان المتغيرات -
var
و
auto
، في حالة ما إذا كنت حقًا تحب الإيجابيات ذات السيارات.
سياق التنفيذ
ChaiScript له سياق محلي وعالمي. يتم تنفيذ الكود من أعلى إلى أسفل من سطر إلى آخر ، ولكن يمكن أخذه في وظائف ويتم استدعاؤه لاحقًا (ولكن ليس في وقت سابق!). المتغيرات المعلنة داخل الدالات أو الشروط / الحلقات غير مرئية افتراضيًا من الخارج ، ولكن يمكنك تغيير هذا السلوك باستخدام المعرف
global
بدلاً من
var
. تختلف المتغيرات العامة عن المتغيرات العادية ، فهي أولاً مرئية خارج السياق المحلي ، وثانياً ، يمكن إعادة الإعلان عنها (إذا لم يتم تحديد القيمة أثناء إعادة الإعلان ، فستظل كما هي)
بالمناسبة ، إذا كان لديك متغير ، وتحتاج إلى التحقق مما إذا كان قد تم تعيين قيمة له ، استخدم
is_var_undef
المضمنة
is_var_undef
، والتي تُرجع إلى الحالة الحقيقية إذا كان المتغير غير معرّف.
سلسلة الاستيفاء
يمكن وضع الكائنات الأساسية أو كائنات المستخدم التي لها طريقة
to_string()
في سلسلة باستخدام بناء جملة
${object}
. هذا يتجنب تسلسلات السلسلة غير الضرورية ويبدو بشكل عام أكثر ملاءمة:
var x = 3 var y = 4
ناقل ، خريطة ، MapPair وجميع البدائية أيضا دعم هذه الميزة. يتم عرض المتجه بالتنسيق
[o1, o2, ...]
، تعيين كـ
[<key1, val1>, <key2, val2>, ...]
، و MapPair:
<key, val>
.
وظائف والفروق الدقيقة
وظائف ChaiScript هي كائنات مثل كل شيء آخر. يمكن التقاطها ، وتعيينها للمتغيرات ، وجعلها متداخلة في وظائف أخرى ، وتمريرها كوسيطة. بالنسبة لهم أيضًا ، يمكنك تحديد نوع قيم الإدخال (وهو ما تفتقر إليه اللغات المكتوبة ديناميكيًا!) ، لهذا تحتاج إلى تحديد النوع قبل إعلان معلمة الوظيفة. إذا تم تحويل المعلمة ، عند الاتصال ، إلى المعلمة المحددة ، فسيحدث التحويل وفقًا لقواعد C ++ ، وإلا يتم طرح استثناء:
def adder(int x, int y) { return x + y } def adder(bool x, bool y) { return x || y } adder(1, 2)
يمكن أيضًا ضبط وظائف اللغة على شروط الاتصال (حارس المكالمة). إذا لم يتم احترامها ، يتم طرح استثناء ، وإلا يتم إجراء مكالمة. وألاحظ أيضًا أنه إذا لم يكن للوظيفة بيان إرجاع في النهاية ، فسيتم إرجاع التعبير الأخير. مريحة للغاية للأعمال الروتينية الصغيرة:
def div(x, y) : y != 0 { x / y }
الفئات و Dynamic_Object
يحتوي ChaiScript على أساسيات OOP ، وهي علامة زائد محددة إذا كنت بحاجة إلى معالجة كائنات معقدة. تحتوي اللغة على نوع خاص - Dynamic_Object. في الواقع ، كل مثيلات الفئات ومساحات الأسماء هي Dynamic_Object تمامًا ذات خصائص محددة مسبقًا. يسمح لك كائن ديناميكي بإضافة حقول إليه أثناء تنفيذ البرنامج النصي ، ثم الوصول إليه:
var obj = Dynamic_Object(); obj.x = 3; obj.f = fun(arg) { print(this.x + arg); }
يتم تعريف الطبقات بكل بساطة. يمكن تعيينها على الحقول والأساليب والمنشئين. من
set_explicit(object, value)
المثيرة للاهتمام
set_explicit(object, value)
خلال الدالة المميزة
set_explicit(object, value)
يمكنك "إصلاح" حقول الكائن عن طريق حظر إضافة أساليب أو سمات جديدة بعد إعلان الفئة (يتم ذلك عادة في المُنشئ):
class Widget { var id;
نقطة مهمة - في الحقيقة ، طرق الفصل هي مجرد وظائف الوسيطة الأولى هي كائن فئة مع نوع محدد بوضوح. لذلك ، يكافئ التعليمة البرمجية التالية إضافة أسلوب إلى فئة موجودة:
def set_id(Widget w, id) { w.id = id } w.set_id(9)
يمكن لأي شخص مطلع على C # أن يحل محل ما يشبه طريقة التمديد ، وسيكون قريبًا من الحقيقة. وبالتالي ، في اللغة ، يمكنك إضافة وظائف جديدة حتى للفئات المدمجة ، على سبيل المثال ، لسلسلة أو int. يقدم المؤلفون أيضًا طريقة صعبة لتحميل المشغلين الزائدين: للقيام بذلك ، تحتاج إلى تطويق رمز المشغل باستخدام علامة التلدة (`) كما في المثال أدناه:
النطاقات
عند الحديث عن مساحة الاسم في ChaiScript ، يجب أن يؤخذ في الاعتبار أن هذه الفئات هي أساسًا دائمًا في سياق عالمي. يمكنك إنشائها باستخدام وظيفة
namespace(name)
، ثم إضافة الوظائف والفئات اللازمة. بشكل افتراضي ، لا توجد مكتبات في اللغة ، ومع ذلك يمكنك تثبيتها باستخدام الملحقات ، والتي سنتحدث عنها لاحقًا. بشكل عام ، قد تبدو تهيئة مساحة الاسم كما يلي:
namespace("math")
تعبيرات لامدا وغيرها من الميزات
تعبيرات Lambda في ChaiScript تشبه ما نعرفه من C ++. يتم استخدام الكلمة الأساسية
الممتعة لهم ، وهي تتطلب أيضًا تحديد المتغيرات التي تم التقاطها بشكل صريح ، ولكنها تفعل ذلك دائمًا بالرجوع إليها. تحتوي اللغة أيضًا على دالة ربط تسمح لك بربط القيم بوظائف المعلمة:
var func_object = fun(x) { x * x } func_object(9)
استثناءات
قد تحدث استثناءات أثناء تنفيذ البرنامج النصي. يمكن اعتراضها في ChaiScript نفسها (والتي سنناقشها هنا) وفي C ++. بناء الجملة مطابق تمامًا للإيجابيات ، حتى يمكنك رمي رقم أو سلسلة:
try { eval(x + 1)
بطريقة جيدة ، يجب عليك تحديد فئة الاستثناءات ورميها. سنتحدث عن كيفية اعتراضها في C ++ في القسم الثاني. بالنسبة إلى استثناءات المترجمين الفوريين ، يلقي ChaiScript استثناءاته ، مثل eval_error ، bad_boxed_cast ، إلخ.
ثوابت المترجم
لدهشتي ، تحولت اللغة إلى نوع من وحدات ماكرو المترجم - هناك 4 منها فقط وكلها تعمل على تحديد السياق وتستخدم في الغالب لمعالجة الأخطاء:
خطأ الملاءمة
إذا لم يتم الإعلان عن الوظيفة التي تتصل بها ، فسيتم طرح استثناء. إذا كان هذا غير مقبول بالنسبة لك ، يمكنك تحديد وظيفة خاصة -
method_missing(object, func_name, params)
، والتي سيتم استدعاؤها باستخدام الوسيطات المقابلة في حالة وجود خطأ:
def method_missing(Widget w, string name, Vector v) { print("widget method ${name} with params {v} was not found") } w = Widget() w.invoke_error(1, 2, 3)
وظائف مدمجة
يحدد ChaiScript العديد من الوظائف المدمجة ، وفي المقالة أود التحدث عن الوظائف المفيدة بشكل خاص. من بينها:
eval(str)
،
eval_file(filename)
،
to_json(object)
،
from_json(str)
:
var x = 3 var y = 5 var res = eval("x * y")
التنفيذ في C ++
تركيب
ChaiScript هو مكتبة رأس - فقط C ++ قالب. وفقًا لذلك ، ستحتاج فقط للتثبيت لإنشاء
مستودع للنسخ أو وضع كل الملفات من
هذا المجلد في مشروعك. نظرًا للاعتماد على IDE ، يتم إجراء كل ذلك بطريقة مختلفة وقد تم وصفه بالتفصيل في المنتديات لفترة طويلة ، ثم سنفترض أنك تمكنت من توصيل المكتبة ، ويتم تضمين التعليمات البرمجية مع:
#include <chaiscript/chaiscript.hpp>
.
استدعاء رمز C ++ وتحميل البرنامج النصي
أصغر نموذج التعليمات البرمجية باستخدام ChaiScript كما هو موضح أدناه. نحدد وظيفة بسيطة في C ++ تأخذ std :: string وتُرجع السلسلة المعدلة ، ثم نضيف رابطًا إليها في كائن ChaiScript لاستدعائه. قد يستغرق التجميع وقتًا كبيرًا ، ولكن هذا يرجع في المقام الأول إلى حقيقة أن إنشاء عدد كبير من قوالب برنامج التحويل البرمجي ليس بالأمر السهل:
#include <string> #include <chaiscript/chaiscript.hpp> std::string greet_name(const std::string& name) { return "hello, " + name; } int main() { chaiscript::ChaiScript chai; // chaiscript chai.add(chaiscript::fun(&greet_name), "greet"); // greet // eval chai.eval(R"( print(greet("John")); )"); }
أتمنى أن تكون قد نجحت ورأيت نتيجة الوظيفة. أريد أن أشير إلى فارق بسيط واحد على الفور - إذا أعلنت أن كائن ChaiScript ثابت ، فإنك تحصل على خطأ وقت تشغيل غير سارة. هذا يرجع إلى حقيقة أن اللغة تدعم تعدد العمليات بشكل افتراضي وتخزين متغيرات التدفق المحلية التي يتم الوصول إليها في destructor الخاص به. ومع ذلك ، يتم تدميرها قبل استدعاء destructor للمثيل الثابت ، ونتيجة لذلك ، لدينا خطأ في الوصول أو خطأ في تجزئة الخطأ. استنادًا إلى
المشكلة على github ، سيكون الحل الأبسط هو وضع
#define CHAISCRIPT_NO_THREADS
في إعدادات برنامج التحويل البرمجي أو قبل تضمين ملف المكتبة ، وبالتالي تعطيل تعدد العمليات. كما أفهمها ، لم يكن من الممكن إصلاح هذا الخطأ.
الآن سوف نحلل بالتفصيل كيف يحدث التفاعل بين C ++ و ChaiScript. تعرّف المكتبة
fun
دالة القالب الخاصة ، والتي يمكن أن تأخذ مؤشرًا إلى دالة أو عامل أو مؤشر إلى متغير فئة ، ثم تُرجع كائنًا خاصًا يخزن الحالة. على سبيل المثال ، دعونا نحدد فئة القطعة في رمز C ++ ونحاول ربطها بـ ChaiScript بطرق مختلفة:
class Widget { int Id; public: Widget(int id) : Id(id) { } int GetId() const { return this->Id; } }; std::string ToString(const Widget& w) { return "widget #" + std::to_string(w.GetId()); } int main() { chaiscript::ChaiScript chai; Widget w(2);
كما ترون ، يعمل ChaiScript بهدوء تام مع فئات C ++ غير المعروفة لها ويمكنه استدعاء أساليبها. إذا ارتكبت خطأ في مكان ما في الكود ، فمن المرجح أن يلقي البرنامج النصي استثناءًا من
error in function dispatch
، وهو أمر غير مهم على الإطلاق. ومع ذلك ، لا يمكن استيراد وظائف فقط ، دعونا نرى كيفية إضافة متغير إلى برنامج نصي باستخدام المكتبة. للقيام بذلك ، حدد المهمة أصعب قليلاً - قم باستيراد std :: vector <Widget>.
chaiscript::var
وظيفة
chaiscript::var
وطريقة
add_global
هذا. سنضيف أيضًا حقل
Data
العام إلى عنصر واجهة المستخدم لدينا لمعرفة كيفية استيراد حقل الفصل:
class Widget { int Id; public: int Data = 0; Widget(int id) noexcept : Id(id) { } int GetId() const { return this->Id; } }; std::string ToString(const Widget& w) { return "widget #" + std::to_string(w.GetId()) + " with data: " + std::to_string(w.Data); int main() { chaiscript::ChaiScript chai; std::vector<Widget> W;
يعرض الرمز أعلاه:
widget #1 with data: 0
، widget #2 with data: 2
، widget #3 with data: 4
. أضفنا مؤشرًا إلى حقل الفصل في ChaiScript ، وبما أن الحقل أصبح نوعًا بدائيًا ، فإننا نغير قيمته. أيضًا ، تمت إضافة عدة طرق للعمل مع
std::vector
، بما في ذلك
operator[]
. يعرف أولئك الذين يعرفون STL أن
std::vector
طريقتان للفهرسة - أحدهما يعيد ارتباط ثابت ، والآخر رابط بسيط. لهذا السبب بالنسبة للوظائف الزائدة ، يجب أن تشير صراحة إلى نوعها - وإلا فإن الغموض ينشأ ، وسيقوم المترجم بإلقاء خطأ.
توفر المكتبة عدة طرق إضافية لإضافة كائنات ، ولكنها كلها متطابقة تقريبًا ، لذلك لا أرى نقطة النظر فيها بالتفصيل. كتلميح صغير ، إليك الكود أدناه:
chai.add(chaiscript::var(x), "x");
باستخدام حاويات STL
إذا كنت ترغب في تمرير حاويات STL التي تحتوي على أنواع
بدائية إلى ChaiScript ، يمكنك إضافة مثيل حاوية حاوية إلى البرنامج النصي الخاص بك بحيث لا تقوم باستيراد طرق لكل نوع.
using MyVector = std::vector<std::pair<int, std::string>>; MyVector V; V.emplace_back(1, "John"); V.emplace_back(3, "Bob");
ChaiScript, . , STL-, . c
std::vector<Widget>
, , , ChaiScript
vector_type
, Widget .
++ ChaiScript
ChaiScript, . , . Widget WindowWidget, , :
class Widget { int Id; public: Widget(int id) : Id(id) { } int GetId() const { return this->Id; } }; class WindowWidget : public Widget { std::pair<int, int> Size; public: WindowWidget(int id, int width, int height) : Widget(id), Size(width, height) { } int GetWidth() const { return this->Size.first; } int GetHeight() const { return this->Size.second; } }; int main() { chaiscript::ChaiScript chai;
ChaiScript , C++ , . - (, ), ,
std::vector<Widget>
.
. ChaiScript , , :
Widget w(3); w.Data = 4444;
أيضًا ، عند تصدير فئات "مكتبة" أكثر من C ++ إلى ChaiScript (على سبيل المثال ، vec3 ، معقدة ، مصفوفة) ، غالبًا ما تكون إمكانية التحويل الضمني من نوع إلى آخر مطلوبة. في ChaiScript ، يتم حل هذه المشكلة عن طريق إضافة type_conversion
برنامج نصي إلى الكائن. على سبيل المثال ، ضع في الاعتبار الفئة المعقدة وتطبيق تحويل int والمضاعفة إليها أثناء الإضافة: class Complex { public: float Re, Im; Complex(float re, float im = 0.0f) : Re(re), Im(im) { } }; int main() { chaiscript::ChaiScript chai;
وبالتالي ، ليس من الضروري كتابة وظيفة تحويل في C ++ نفسها ، وبعد ذلك فقط تصديرها إلى ChaiScript. يمكنك إضافة تحويلات ووصف الوظيفة الجديدة بالفعل في رمز البرنامج النصي نفسه. إذا كان التحويل للنوعين غير بديهي ، يمكنك تمرير lambda كوسيطة إلى دالة type_conversion
. وسوف يطلق عليه عندما الصب.يتم استخدام مبدأ مماثل لتحويل Vector أو Map ChaiScript إلى نوعك المخصص. لهذا ، vector_conversion
ويتم تعريفها في المكتبة map_conversion
.تفريغ ChaiScript إرجاع القيم
eval
eval_file
Boxed_Value
. C++, ,
boxed_cast<T>
. , ,
bad_boxed_cast
:
ChaiScript shared_ptr, . shared_ptr :
auto x = chai.eval<std::shared_ptr<double>>("var x = 3.2");
الشيء الرئيسي هو عدم الاحتفاظ بمرجع إلى قيمة Shared_ptr المؤجلة ، وإلا فإنك تواجه خطر الحصول على انتهاك الوصول بعد حذف المتغير أثناء تجميع البيانات المهملة تلقائيًا في البرنامج النصي.مثل المتغيرات ، يمكنك الحصول على وظائف من ChaiScript في شكل عوامل توجيه معبأة تلتقط حالة كائن ChaiScript. على سبيل المثال ، سوف نستخدم الوظيفة التي تم تنفيذها بالفعل للفئة Complex وحاول استخدامها لاستدعاء وظيفة في مرحلة تنفيذ البرنامج: auto printComplex = chai.eval<std::function<void(Complex)>>(R"( fun(Complex c) { print("${c.re} + ${c.im}i"); } )");
استثناء ChaiScript اصطياد
, .
eval_error
,
bad_boxed_cast
,
std::exception
. , ++:
class MyException : public std::exception { public: int Data; MyException(int data) : std::exception("MyException"), Data(data) { } }; int main() { chaiscript::ChaiScript chai;
, C++.
pretty_print
,
eval_error
, , , , .
ChaiScript
لسوء الحظ ، بشكل افتراضي ، ChaiScript لا يوفر وظائف إضافية من حيث المكتبات. على سبيل المثال ، يفتقر إلى الدوال الرياضية وجداول التجزئة ومعظم الخوارزميات. يمكنك تنزيل بعضها في شكل مكتبات الوحدات النمطية من مستودع ChaiScript Extras الرسمي ، ثم الاستيراد إلى البرنامج النصي الخاص بك. على سبيل المثال ، خذ مكتبة الرياضيات ووظيفة acos (x): #include <chaiscript/chaiscript.hpp> #include <chaiscript/extras/math.hpp> int main() { chaiscript::ChaiScript chai; // auto mathlib = chaiscript::extras::math::bootstrap(); chai.add(mathlib); std::cout << chai.eval<double>("acos(0.5)"); // ~1.047 }
يمكنك أيضًا كتابة مكتبتك للغة ثم الاستيراد. يتم ذلك بكل بساطة ، لذلك أنصحك بقراءة الرياضيات مفتوحة المصدر أو أي مصدر آخر في المستودع. من حيث المبدأ ، كجزء من التكامل مع C ++ ، درسنا كل شيء تقريبًا ، لذلك أعتقد أنه يمكن إكمال القسم في هذا الشأن.تجربة شخصية
3D- OpenGL , , .
, , , « », .
, ChaiScript , Lua. , , : , C++ C, - C-style . , , .
, . ImGui, chaiscript. , :
, -, . :
chaiscript ImGui:, . , Lua , , (JIT ), ChaiScript . , , .
. C++ ( Lua ), ChaiScript . . .
روابط مفيدة