ChaiScript - bahasa scripting untuk C ++

Ketika ada kebutuhan untuk menanamkan bahasa scripting dalam proyek C ++, hal pertama yang paling diingat orang adalah Lua. Dalam artikel ini tidak akan, saya akan berbicara tentang yang lain, tidak kurang nyaman dan mudah dipelajari bahasa yang disebut ChaiScript.

gambar

Pengantar singkat


Saya sendiri menemukan ChaiScript secara kebetulan ketika saya menyaksikan salah satu ceramah Jason Turner, salah satu pencipta bahasa tersebut. Itu membuat saya tertarik, dan pada saat itu ketika perlu untuk memilih bahasa scripting dalam proyek, saya memutuskan - mengapa tidak mencoba ChaiScript? Hasilnya mengejutkan saya (pengalaman pribadi saya akan ditulis lebih dekat ke akhir artikel), namun, tidak peduli betapa aneh kedengarannya, tidak ada satu artikel pun di hub yang bahkan menyebutkan bahasa ini, dan saya memutuskan bahwa alangkah baiknya menulis tentang dia. Tentu saja, bahasanya memiliki dokumentasi dan situs resmi , tetapi tidak semua orang akan membacanya dari pengamatan, dan format artikel lebih dekat dengan banyak orang (termasuk saya).

Pertama, kita akan berbicara tentang sintaks bahasa dan semua fitur-fiturnya, kemudian tentang bagaimana mengimplementasikannya dalam proyek C ++ Anda, dan pada akhirnya saya akan berbicara sedikit tentang pengalaman saya. Jika beberapa bagian dari Anda tidak tertarik, atau Anda ingin membaca artikel dalam urutan yang berbeda, Anda dapat menggunakan daftar isi:



Sintaks bahasa


ChaiScript sangat mirip dengan C ++ dan JS dalam sintaksnya. Pertama-tama, itu, seperti sebagian besar bahasa scripting, diketik secara dinamis, namun, tidak seperti JavaScript, ia memiliki pengetikan yang ketat (no 1 + "2" ). Ada juga pengumpul sampah bawaan, bahasanya sepenuhnya dapat ditafsirkan, memungkinkan Anda untuk mengeksekusi kode per baris, tanpa kompilasi ke dalam bytecode. Ini memiliki dukungan untuk pengecualian (apalagi, bersama, memungkinkan Anda untuk menangkap keduanya di dalam skrip dan di C ++), fungsi lambda, operator overloading. Itu tidak sensitif terhadap spasi, memungkinkan Anda untuk menulis sebagai satu baris melalui titik koma, atau dengan gaya python, memisahkan ekspresi dengan baris baru.

Tipe primitif


ChaiScript secara default menyimpan variabel integer sebagai int, real double, dan string dengan std :: string. Ini dilakukan terutama untuk memastikan kompatibilitas dengan kode panggilan. Bahasa tersebut bahkan memiliki akhiran untuk angka, sehingga kami dapat secara eksplisit menunjukkan jenis variabel kami:

 /*   chaiscript    js    ,  var / auto `;`      */ var myInt = 1 // int var myLongLong = 1ll // long long int var myFloating = 3.3 // double var myBoolean = false // bool var myString = "hello world!\n" // std::string 

Mengubah jenis variabel tidak berfungsi, kemungkinan besar Anda perlu mendefinisikan operator `=` Anda sendiri untuk jenis-jenis ini, jika tidak Anda berisiko melempar pengecualian (kita akan membicarakan ini nanti) atau menjadi korban pembulatan, seperti ini:

 var integer = 3 integer = 5.433 print(integer) //  5    double    int! integer = true //   -   `=`  (int, bool) 

Namun, Anda dapat mendeklarasikan variabel tanpa memberikan nilai padanya, dalam hal ini ia akan berisi jenis yang tidak terdefinisi sampai variabel tersebut diberi nilai.

Wadah sebaris


Bahasa ini memiliki dua wadah - Vektor dan Peta. Mereka bekerja sangat mirip dengan rekan-rekan mereka di C ++ (std :: vector dan std :: map, masing-masing), tetapi mereka tidak memerlukan tipe, karena mereka dapat menyimpan apa pun. Pengindeksan dapat dilakukan seperti biasa dengan int, tetapi Peta membutuhkan kunci dengan string. Tampaknya terinspirasi oleh python, penulis juga menambahkan kemampuan untuk dengan cepat mendeklarasikan kontainer dalam kode menggunakan sintaks berikut:

 var v = [ 1, 2, 3u, 4ll, "16", `+` ] //      var m = [ "key1" : 1, "key2": "Bob" ]; //    - var M = Map() //    var V = Vector() //    //        C++ : v.push_back(123) //    ,     v.push_back_ref(m); // m -   //      m["key"] = 3 //       (reference assignment): m["key"] := m //       

Kedua kelas ini hampir sepenuhnya mengulangi analog mereka dalam C ++, dengan pengecualian iterators, karena alih-alih mereka ada Range dan Const_Range kelas khusus. By the way, semua kontainer dilewatkan dengan referensi bahkan jika Anda menggunakan penugasan melalui =, yang sangat aneh bagi saya, karena untuk semua jenis lainnya, penyalinan menurut nilai terjadi.

Konstruksi Bersyarat


Hampir semua konstruksi kondisi dan siklus dapat dijelaskan secara harfiah dalam satu contoh kode:

 var a = 5 var b = -1 //  if-else if (a > b) { print("a > b") } else if (a == b){ print("a == b") } else { print("a < b") } // switch -    if- //      //  break    ,    C++ var str = "hello" switch(str) { case("hi") { print("hi!"); break; } case("hello") { print("hello!" break; } case("bye") { print("bye-bye!") break; } default { print("what have you said?") } } var x = true //     ,       while (x) { print("x was true") x = false; } //    C.        ,    ,    ,    for (var i = 0; i < 10; ++i) //   -,    { print(i); //  0 ... 9  10  } // ranged-for loop for(element : [1, 2, 3, 4, 5]) { puts(element) //   12345 } //  :   C++17 if-init statements: if(var x = get_value(); x < 10) { print(x) // x     if } 

Saya pikir orang yang akrab dengan C ++ belum menemukan sesuatu yang baru. Ini tidak mengherankan, karena ChaiScript diposisikan sebagai bahasa yang mudah bagi "pelajar" untuk belajar, dan karena itu meminjam semua desain klasik yang terkenal. Penulis memutuskan untuk menyoroti bahkan dua kata kunci untuk mendeklarasikan variabel - var dan auto , jika Anda benar-benar menyukai plus dengan otomatis.

Konteks eksekusi


ChaiScript memiliki konteks lokal dan global. Kode dijalankan dari atas ke bawah baris demi baris, namun dapat diambil dalam fungsi dan dipanggil nanti (tetapi tidak lebih awal!). Variabel yang dideklarasikan di dalam fungsi atau kondisi / loop tidak terlihat secara default dari luar, tetapi Anda dapat mengubah perilaku ini menggunakan pengidentifikasi global alih-alih var . Variabel global berbeda dari yang biasa dalam hal itu, pertama, mereka terlihat di luar konteks lokal, dan kedua, mereka dapat dinyatakan kembali (jika nilainya tidak ditetapkan selama deklarasi ulang, maka ia tetap sama)

 //     chaiscript def foo(x) { global G = 2 print(x) } foo(0) //  foo(x), G = 2 print(G) //  2 global G = 3 //  G = 3,   global -  ! 

Omong-omong, jika Anda memiliki variabel, dan Anda perlu memeriksa apakah suatu nilai ditetapkan untuknya, gunakan fungsi is_var_undef yang mengembalikan true jika variabel tidak terdefinisi.

Interpolasi string


Objek dasar atau objek pengguna yang memiliki metode to_string() dapat dimasukkan ke dalam string menggunakan sintaks ${object} . Ini menghindari rangkaian string yang tidak perlu dan umumnya terlihat jauh lebih rapi:

 var x = 3 var y = 4 //  sum of 3 + 4 = 7 print("sum of ${x} + ${y} = ${x + y}") 

Vektor, Peta, MapPair dan semua primitif juga mendukung fitur ini. Vektor ditampilkan dalam format [o1, o2, ...] , Peta sebagai [<key1, val1>, <key2, val2>, ...] , dan MapPair: <key, val> .

Fungsi dan nuansa mereka


Fungsi chaiScript adalah objek seperti yang lainnya. Mereka dapat ditangkap, ditugaskan ke variabel, dibuat bersarang di fungsi lain, dan diteruskan sebagai argumen. Juga untuk mereka, Anda dapat menentukan jenis nilai input (yang tidak dimiliki oleh bahasa yang diketik secara dinamis!), Untuk ini, Anda perlu menentukan jenisnya sebelum mendeklarasikan parameter fungsi. Jika, ketika dipanggil, parameter dapat dikonversi ke yang ditentukan, maka konversi akan terjadi sesuai dengan aturan C ++, jika tidak pengecualian akan dilemparkan:

 def adder(int x, int y) { return x + y } def adder(bool x, bool y) { return x || y } adder(1, 2) // ,  3 adder(1.22, -3.7) // ,  1 + (-3) = 2 adder(true, true) // ,  true adder(true, 3) // ,    adder(bool, int) 

Fungsi dalam bahasa juga dapat mengatur kondisi panggilan (penjaga panggilan). Jika tidak dihormati, pengecualian dilemparkan, jika tidak panggilan akan dilakukan. Saya juga mencatat bahwa jika fungsi tidak memiliki pernyataan pengembalian di akhir, maka ekspresi terakhir akan dikembalikan. Sangat nyaman untuk rutinitas kecil:

 def div(x, y) : y != 0 { x / y } //  `y`    -    `x`  `y` print(div(2, 0.5)) //  4.0 print(div(2, 0)) // , `y`  0! 

Kelas dan Dynamic_Object


ChaiScript memiliki dasar-dasar OOP, yang merupakan nilai tambah pasti jika Anda perlu memanipulasi objek yang kompleks. Bahasa ini memiliki tipe khusus - Dynamic_Object. Bahkan, semua instance dari kelas dan ruang nama persis Dynamic_Object dengan properti yang telah ditentukan. Objek dinamis memungkinkan Anda untuk menambahkan bidang selama eksekusi skrip, dan kemudian mengaksesnya:

 var obj = Dynamic_Object(); obj.x = 3; obj.f = fun(arg) { print(this.x + arg); } //  obj   f (     `x` obj.f(-3); //  0 

Kelas didefinisikan dengan cukup sederhana. Mereka dapat diatur ke bidang, metode, konstruktor. Dari set_explicit(object, value) menarik set_explicit(object, value) melalui fungsi khusus set_explicit(object, value) Anda dapat "memperbaiki" bidang objek dengan melarang penambahan metode atau atribut baru setelah deklarasi kelas (ini biasanya dilakukan di konstruktor):

 class Widget { var id; //  id def Widget() { this.id= 0 } //    def Widget(id) { this.id = id } //   1  def get_id() { id } //   } var w = Widget(10) print(w.get_id()) //  10 (w.id) print(w.get_id) //   10,        set_explicit(w, true) //    wx = 3 //      Widget   x 

Poin penting - pada kenyataannya, metode kelas hanya fungsi yang argumen pertamanya adalah objek dari kelas dengan tipe yang ditentukan secara eksplisit. Oleh karena itu, kode berikut ini setara dengan menambahkan metode ke kelas yang ada:

 def set_id(Widget w, id) { w.id = id } w.set_id(9) // w.id = 9 set_id(w, 9) //  , w.id = 9 

Siapa pun yang akrab dengan C # dapat menggantikan apa yang tampak menyakitkan seperti metode ekstensi, dan akan mendekati kebenaran. Dengan demikian, dalam bahasa Anda dapat menambahkan fungsionalitas baru bahkan untuk kelas bawaan, misalnya, untuk string atau int. Penulis juga menawarkan cara yang rumit untuk membebani operator: untuk melakukan ini, Anda perlu mengelilingi simbol operator dengan tilde (`) seperti pada contoh di bawah ini:

 //   +     Widget def `+`(Widget w1, Widget w2) { print("merging two widgets!") } var widget1 = Widget() var widget2 = Widget() widget1 + widget2 //      //        : var plus = `+` print(plus(1, 7)) //  8 

Ruang nama


Berbicara tentang namespace di ChaiScript, harus diingat bahwa ini pada dasarnya adalah kelas yang selalu dalam konteks global. Anda bisa membuatnya menggunakan fungsi namespace(name) , dan kemudian menambahkan fungsi dan kelas yang diperlukan. Secara default, tidak ada perpustakaan dalam bahasa ini, namun Anda dapat menginstalnya menggunakan ekstensi, yang akan kita bicarakan sedikit kemudian. Secara umum, inisialisasi namespace mungkin terlihat seperti ini:

 namespace("math") //    math //   math.square = fun(x) { x * x } math.hypot_squared= fun(x, y) { math.square(x) + math.square(y) } print(math.square(4)) //  16 print(math.hypot_squared(3, 4)) //  25 

Ekspresi Lambda dan fitur lainnya


Ekspresi Lambda di ChaiScript mirip dengan apa yang kita ketahui dari C ++. Kata kunci yang menyenangkan digunakan untuk mereka, dan mereka juga membutuhkan secara eksplisit menentukan variabel yang ditangkap, namun mereka selalu melakukan ini dengan referensi. Bahasa ini juga memiliki fungsi bind yang memungkinkan Anda untuk mengikat nilai ke parameter fungsi:

 var func_object = fun(x) { x * x } func_object(9) //  81 var name = "John" var greet = fun[name]() { "Hello, " + name } print(greet()) //  Hello, John name = "Bob" print(greet()) //  Hello, Bob var message = bind(fun(msg, name) { msg + " from " + name }, _, "ChaiScript"); print(message("Hello")) //  Hello from ChaiScript 

Pengecualian


Pengecualian dapat terjadi selama eksekusi skrip. Mereka dapat dicegat baik dalam ChaiScript itu sendiri (yang akan kita bahas di sini) dan di C ++. Sintaksnya benar-benar identik dengan plus, Anda bahkan dapat membuang angka atau string:

 try { eval(x + 1) // x   } catch (e) { print("Error during evaluation")) } //   C++   ChaiScript //   Vector -   std::vector,    std::exception      try { var vec = [1, 2] var val = vec[3] //     } catch (e) { print("index out of range: " + e.what()); // e.what    ChaiScript } //  atch   guard     ,    `:` try { throw(5.2) } catch(e) : is_type(e, "int") { print("Int: ${e}"); //   `e`  int } catch(e) : is_type(e, "double") { print("Double: ${e}"); //  `e`  double } 

Dengan cara yang baik, Anda harus mendefinisikan kelas pengecualian dan membuangnya. Kami akan berbicara tentang cara mencegatnya di C ++ di bagian kedua. Untuk pengecualian juru bahasa, ChaiScript melempar pengecualiannya, seperti eval_error, bad_boxed_cast, dll.

Konstanta juru bahasa


Yang mengejutkan saya, bahasanya ternyata semacam makro kompiler - hanya ada 4 dan semuanya berfungsi untuk mengidentifikasi konteks dan sebagian besar digunakan untuk penanganan kesalahan:
__LINE__baris saat ini, jika kode tidak dieksekusi dari file, maka '1'
__FILE__file saat ini, jika kode tidak dipanggil dari file, maka "__EVAL__"
__CLASS__kelas saat ini atau "NOT_IN_CLASS"
__FUNC__fungsi saat ini atau "NOT_IN_FUNCTION"

Terjebak kesalahan


Jika fungsi yang Anda panggil belum diumumkan, pengecualian dilemparkan. Jika ini tidak dapat diterima untuk Anda, Anda dapat mendefinisikan fungsi khusus - method_missing(object, func_name, params) , yang akan dipanggil dengan argumen yang sesuai jika terjadi kesalahan:

 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) //  widget method invoke_error with params [1, 2, 3] was not found 

Fungsi bawaan


ChaiScript mendefinisikan banyak fungsi built-in, dan dalam artikel ini saya ingin berbicara tentang yang sangat berguna. Diantaranya: eval(str) , eval_file(filename) , to_json(object) , from_json(str) :

 var x = 3 var y = 5 var res = eval("x * y") // res = 15,  eval     //     : //  eval_file eval_file("source.chai") //   use,  ,         use("source.chai") // to_json    Map    var w = Widget(0) var j = to_json(w) // j = "{ "id" : 0 }" // from_json    Map ( ,   ) var m = from_json(" { "x": 0, "y": 3, "z": 2 }") print(m) //  Map  [<x, 0>, <y, 3>, <z, 2>] 


Implementasi dalam C ++


Instalasi


ChaiScript adalah pustaka hanya header berbasis template C ++. Dengan demikian, untuk instalasi Anda hanya perlu membuat repositori clone atau hanya memasukkan semua file dari folder ini ke proyek Anda. Karena, tergantung pada IDE, semua ini dilakukan secara berbeda dan telah dideskripsikan secara rinci di forum untuk waktu yang lama, maka kami akan menganggap bahwa Anda berhasil menghubungkan pustaka, dan kode dengan menyertakan: #include <chaiscript/chaiscript.hpp> dikompilasi.

Doa kode C ++ dan pemuatan skrip


Kode sampel terkecil menggunakan ChaiScript adalah seperti yang ditunjukkan di bawah ini. Kami mendefinisikan fungsi sederhana dalam C ++ yang mengambil std :: string dan mengembalikan string yang diubah, dan kemudian kami menambahkan tautan ke dalamnya di objek ChaiScript untuk memanggilnya. Kompilasi dapat memakan waktu yang cukup lama, tetapi ini terutama disebabkan oleh fakta bahwa instantiasi sejumlah besar templat untuk kompiler tidaklah mudah:

 #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")); )"); } 

Saya harap Anda berhasil, dan Anda melihat hasil dari fungsinya. Saya ingin segera mencatat satu nuansa - jika Anda mendeklarasikan objek ChaiScript sebagai statis, Anda mendapatkan kesalahan runtime yang tidak menyenangkan. Ini disebabkan oleh fakta bahwa bahasa mendukung multithreading secara default dan menyimpan variabel aliran lokal yang diakses di destruktornya. Namun, mereka dihancurkan sebelum destruktor instance statis dipanggil, dan sebagai hasilnya, kami memiliki pelanggaran akses atau kesalahan kesalahan segmentasi. Berdasarkan masalah pada github , solusi paling sederhana adalah dengan cukup meletakkan #define CHAISCRIPT_NO_THREADS dalam pengaturan kompiler atau sebelum memasukkan file perpustakaan, sehingga menonaktifkan multithreading. Seperti yang saya pahami, itu tidak mungkin untuk memperbaiki kesalahan ini.

Sekarang kita akan menganalisis secara terperinci bagaimana interaksi antara C ++ dan ChaiScript terjadi. Pustaka mendefinisikan fun fungsi templat khusus, yang dapat membawa pointer ke fungsi, functor, atau pointer ke variabel kelas, dan lalu mengembalikan objek khusus yang menyimpan status. Sebagai contoh, mari kita mendefinisikan kelas Widget dalam kode C ++ dan mencoba mengaitkannya dengan ChaiScript dengan cara yang berbeda:

 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); //  Widget  C++  chai.add(chaiscript::fun([&w] { return w; }), "get_widget"); //         chai.add(chaiscript::fun(ToString), "to_string"); //   chai.add(chaiscript::fun(&Widget::GetId), "get_id"); //   //    ,   Widget    GetId,    to_string,    chai.eval(R"( var w = get_widget() print(w.get_id) //  2 print(w) //  widget #2 )"); } 

Seperti yang Anda lihat, ChaiScript bekerja dengan sangat tenang dengan kelas C ++ yang tidak diketahui dan dapat memanggil metode mereka. Jika Anda membuat kesalahan di suatu tempat dalam kode, kemungkinan besar skrip akan melemparkan pengecualian error in function dispatch jenis error in function dispatch , yang sama sekali tidak kritis. Namun, tidak hanya fungsi yang dapat diimpor, mari kita lihat cara menambahkan variabel ke skrip menggunakan pustaka. Untuk melakukan ini, pilih tugas sedikit lebih keras - import std :: vector <Widget>. Fungsi chaiscript::var dan metode add_global akan membantu kami dalam hal ini. Kami juga akan menambahkan bidang publik Data ke Widget kami untuk melihat cara mengimpor bidang kelas:

 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 W.emplace_back(1); W.emplace_back(2); W.emplace_back(3); chai.add(chaiscript::fund(ToString), "to_string"); chai.add(chaiscript::fun(&Widget::Data), "data"); //     //     ChaiScript chai.add_global(chaiscript::var(std::ref(W)), "widgets"); //     std::ref chai.add(chaiscript::fun(&std::vector<Widget>::size), "size"); //   // .        using IndexFuncType = Widget& (std::vector<Widget>::*)(const size_t); chai.add(chaiscript::fun(IndexFuncType(&std::vector<Widget>::operator[])), "[]"); chai.eval(R"( for(var i = 0; i < vec.size; ++i) { vec[i].data = i * 2; print(vec[i]) } )"); } 

Kode di atas menampilkan: widget #1 with data: 0 , widget #2 with data: 2 , widget #3 with data: 4 . Kami menambahkan pointer ke bidang kelas di ChaiScript, dan karena bidang ternyata menjadi tipe primitif, kami mengubah nilainya. Juga, beberapa metode ditambahkan untuk bekerja dengan std::vector , termasuk operator[] . Mereka yang terbiasa dengan STL tahu bahwa std::vector dua metode pengindeksan - satu mengembalikan tautan yang konstan, yang lain merupakan tautan sederhana. Itulah sebabnya untuk fungsi kelebihan beban, Anda harus secara eksplisit menunjukkan jenisnya - jika tidak, ambiguitas muncul, dan kompiler akan membuat kesalahan.

Perpustakaan menyediakan beberapa metode lagi untuk menambahkan objek, tetapi semuanya hampir identik, jadi saya tidak melihat gunanya mempertimbangkannya secara rinci. Sebagai petunjuk kecil, berikut adalah kode di bawah ini:

 chai.add(chaiscript::var(x), "x"); // x   ChaiScript chai.add(chaiscript::var(std::ref(x), "x"); //  ,    C++  ChaiScript auto shared_x = std::make_shared<int>(5); chai.add(chaiscript::var(shared_x), "x"); // shared_ptr      C++  ChaiScript chai.add(chaiscript::const_var(x), "x"); //   ChaiScript    chai.add_global_const(chaiscript::const_var(x), "x"); // global const . ,  x   chai.add_global(chaiscript::var(x), "x"); // global , .  x   chai.set_global(chaiscript::var(x), "x"); //   global ,    const 

Menggunakan STL Containers


Jika Anda ingin meneruskan wadah STL yang berisi tipe primitif ke ChaiScript, Anda bisa menambahkan instantiasi wadah template ke skrip Anda sehingga Anda tidak mengimpor metode untuk setiap jenis.

 using MyVector = std::vector<std::pair<int, std::string>>; MyVector V; V.emplace_back(1, "John"); V.emplace_back(3, "Bob"); //    - vector  pair chai.add(chaiscript::bootstrap::standard_library::vector_type<MyVector>("MyVec")); chai.add(chaiscript::bootstrap::standard_library::pair_type<MyVector::value_type>("MyVecData")); chai.add(chaiscript::var(std::ref(V)), "vec"); chai.eval(R"( for(var i = 0; i < vec.size; ++i) { print(to_string(vec[i].first) + " " + vec[i].second) } )"); 

Di bawah tenda, beberapa fungsi ChaiScript dipanggil, yang dengan sendirinya menambahkan metode yang diperlukan. Secara umum, jika kelas Anda mendukung operasi serupa dengan wadah STL, Anda juga dapat menambahkannya dengan cara ini. Dalam kasus c, std::vector<Widget>ini, sayangnya, tidak mungkin, karena ChaiScript membutuhkan konstruktor tanpa parameter untuk elemen vector_type, yang tidak dimiliki oleh Widget kami.

Kelas C ++ di dalam ChaiScript


Mungkin sebagai bagian dari tugas Anda, Anda tidak hanya perlu memodifikasi objek dalam ChaiScript, tetapi juga membuatnya dalam skrip. Yah, ini sepenuhnya mungkin. Mari kita ambil kelas Widget lagi sebagai contoh dan mewarisi kelas WindowWidget darinya, dan kemudian menambahkan ke skrip kemampuan untuk membuat keduanya dan juga mengonversi kelas yang diwarisi ke basis:

 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; //   Widget    chai.add(chaiscript::user_type<Widget>(), "Widget"); chai.add(chaiscript::constructor<Widget(int)>(), "Widget"); //   WindowWidget    chai.add(chaiscript::user_type<WindowWidget>(), "WindowWidget"); chai.add(chaiscript::constructor<WindowWidget(int, int, int)>(), "WindowWidget"); // ,  Widget -    WindowWidget chai.add(chaiscript::base_class<Widget, WindowWidget>()); //   Widget  WindowWidget chai.add(chaiscript::fun(&Widget::GetId), "get_id"); chai.add(chaiscript::fun(&WindowWidget::GetWidth), "width"); chai.add(chaiscript::fun(&WindowWidget::GetHeight), "height"); //  WindowWidget     chai.eval(R"( var window = WindowWidget(1, 800, 600) print("${window.width} * ${window.height}") print("widget.id is ${window.get_id}") )"); } 

Polimorfisme bekerja di ChaiScript dengan cara yang persis sama seperti di C ++ untuk jenis yang Anda berikan informasi tentang. Jika karena alasan tertentu ada ambiguitas ketika menambahkan pointer ke metode yang diwarisi (mungkin kelas diwarisi dari beberapa metode dasar sekaligus), bawa ke kelas yang diinginkan secara eksplisit, seperti yang dilakukan dalam contoh di atas dengan operator pengindeksan std::vector<Widget>.

Mengikat instance ke metode dan mengonversi tipe


Untuk objek tunggal, akan lebih mudah untuk menggunakan pengambilan tautan ke mereka bersama-sama dengan metode atau bidang. Dalam hal ini, dalam ChaiScript kita mendapatkan fungsi atau variabel global yang dapat diakses tanpa menyebutkan objek ini:

 Widget w(3); w.Data = 4444; //  Widget w chai.add(chaiscript::fun(&Widget::GetId, &w), "widget_id"); chai.add(chaiscript::fun(&Widget::Data, &w), "widget_data"); chai.eval(R"( print(widget_id) print(widget_data) )"); 

Juga, ketika mengekspor lebih banyak "perpustakaan" kelas dari C ++ ke ChaiScript (misalnya, vec3, complex, matrix), kemungkinan konversi implisit dari satu jenis ke yang lain sering diperlukan. Dalam ChaiScript, masalah ini diselesaikan dengan menambahkan type_conversionskrip ke objek. Sebagai contoh, pertimbangkan kelas Complex dan implementasi konversi int dan gandakan untuk itu selama penambahan:

 class Complex { public: float Re, Im; Complex(float re, float im = 0.0f) : Re(re), Im(im) { } }; int main() { chaiscript::ChaiScript chai; //  Complex,   re, im,    `=` chai.add(chaiscript::user_type<Complex>(), "Complex"); chai.add(chaiscript::bootstrap::standard_library::assignable_type<Complex>("Complex")); chai.add(chaiscript::constructor<Complex(float, float)>(), "Complex"); chai.add(chaiscript::fun(&Complex::Re), "re"); chai.add(chaiscript::fun(&Complex::Im), "im"); //     double  int  Complex chai.add(chaiscript::type_conversion<int, Complex>()); chai.add(chaiscript::type_conversion<double, Complex>()); //     `+`    chai.eval(R"( def `+`(Complex c, x) { var res = Complex(0, 0) res.re = c.re + x.re res.im = c.im + x.im return res } var c = Complex(1, 2) c = c + 3 print("${c.re} + ${c.im}i") )"); // : `4 + 2i` } 

Jadi, tidak perlu menulis fungsi konversi dalam C ++ itu sendiri, dan hanya kemudian mengekspornya ke ChaiScript. Anda dapat menambahkan transformasi, dan sudah menjelaskan fungsionalitas baru dalam kode skrip itu sendiri. Jika konversi untuk kedua jenis ini nontrivial, Anda bisa meneruskan lambda sebagai argumen ke suatu fungsi type_conversion. Ini akan dipanggil saat casting.

Prinsip serupa digunakan untuk mengubah Vector atau Map ChaiScript menjadi tipe khusus Anda. Untuk ini, vector_conversiondan didefinisikan di perpustakaan map_conversion.

Membongkar Nilai Pengembalian ChaiScript


Metode evaldan eval_filemengembalikan nilai ekspresi yang terakhir dieksekusi sebagai objek Boxed_Value. Untuk membukanya dan menggunakan hasil dalam kode C ++, Anda dapat secara eksplisit menentukan jenis nilai kembali, atau menggunakan fungsi boxed_cast<T>. Jika konversi antara jenis ada, itu akan dieksekusi, jika tidak pengecualian akan dimunculkan bad_boxed_cast:

 //       double d = chai.eval<double>("5.3 + 2.1"); //     Boxed_Value,     auto v = chai.eval("5.3 + 2.1"); double d = chai.boxed_cast<double>(v); 

Karena semua objek di dalam ChaiScript disimpan menggunakan shared_ptr, Anda bisa mendapatkan objek sebagai penunjuk untuk dikerjakan lebih lanjut. Untuk melakukan ini, tentukan secara eksplisit tipe shared_ptr saat mengonversi nilai kembali:

 auto x = chai.eval<std::shared_ptr<double>>("var x = 3.2"); 

Hal utama adalah tidak menyimpan referensi ke nilai shared_ptr dereferenced, jika tidak Anda berisiko mengambil pelanggaran akses setelah variabel dihapus selama pengumpulan sampah otomatis dalam skrip.

Seperti variabel, Anda bisa mendapatkan fungsi dari ChaiScript dalam bentuk functors dikemas yang menangkap keadaan objek ChaiScript. Misalnya, kita akan menggunakan fungsionalitas kelas Complex yang sudah diterapkan dan mencoba menggunakannya untuk memanggil fungsi pada tahap pelaksanaan program:

 auto printComplex = chai.eval<std::function<void(Complex)>>(R"( fun(Complex c) { print("${c.re} + ${c.im}i"); } )"); //  ,   ,      C++ printComplex(Complex(2, 3)); //  chaiscript,  `2 + 3i` 

Pengecualian ChaiScript menangkap


Penulis menyarankan untuk menangkap tiga jenis pengecualian selain yang Anda hasilkan sendiri. Ini eval_erroruntuk kesalahan runtime, bad_boxed_castyang disebut ketika nilai-nilai kembali dibongkar salah dan std::exceptionuntuk semua yang lain. Jika Anda berencana untuk membuang pengecualian Anda sendiri, Anda dapat mengonfigurasi konversi otomatis ke tipe C ++:

 class MyException : public std::exception { public: int Data; MyException(int data) : std::exception("MyException"), Data(data) { } }; int main() { chaiscript::ChaiScript chai; //      chaiscript chai.add(chaiscript::user_type<MyException>(), "MyException"); chai.add(chaiscript::constructor<MyException(int)>(), "MyException"); try { //          chai.eval("throw(MyException(11111))", chaiscript::exception_specification<MyException, std::exception>()); } catch (MyException& e) { std::cerr << e.Data; //   `11111` } catch (chaiscript::exception::eval_error& e) { std::cerr << e.pretty_print(); } catch(std::exception& e) { std::cerr << e.what(); } } 

Contoh di atas menunjukkan cara menangkap sebagian besar pengecualian di C ++. Selain metode ini pretty_print, eval_errormasih ada banyak data berguna, seperti tumpukan panggilan, nama file, detail kesalahan, tetapi kami tidak akan membahas kelas ini terlalu banyak dalam artikel ini.

Perpustakaan ChaiScript


Sayangnya, secara default, ChaiScript tidak menyediakan fungsionalitas tambahan dalam hal perpustakaan. Misalnya, ia tidak memiliki fungsi matematika, tabel hash, dan sebagian besar algoritma. Anda dapat mengunduh beberapa di antaranya dalam bentuk pustaka modul dari repositori resmi ChaiScript Extras , dan kemudian mengimpor ke dalam skrip Anda. Misalnya, ambil perpustakaan matematika dan fungsi 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 } 

Anda juga dapat menulis perpustakaan Anda untuk bahasa lalu mengimpor. Ini dilakukan cukup sederhana, jadi saya menyarankan Anda untuk membiasakan diri dengan matematika open source atau sumber lain di repositori. Pada prinsipnya, sebagai bagian dari integrasi dengan C ++, kami memeriksa hampir semuanya, jadi saya pikir bagian ini dapat diselesaikan.

Pengalaman pribadi


Saat ini saya sedang menulis mesin 3D di bawah OpenGL sebagai proyek pribadi, dan saya memiliki ide yang sepenuhnya logis untuk mengimplementasikan konsol debugging untuk mengendalikan keadaan aplikasi secara real time melalui perintah. Tentu saja mungkin untuk bersepeda , tetapi seperti yang mereka katakan, "permainan tidak akan sepadan dengan lilin", jadi saya memutuskan untuk mengambil perpustakaan yang sudah selesai.

Seperti yang saya sebutkan di awal artikel, saya sudah tahu tentang ChaiScript, jadi saya punya pilihan antara dia dan Lua. Sampai saat itu, saya tidak terbiasa dengan kedua bahasa tersebut, oleh karena itu, faktor-faktor seperti: sintaks yang jelas, kemudahan penyisipan ke dalam kode yang ada, dan dukungan untuk C ++ alih-alih yang paling dipengaruhi oleh C agar tidak membuat pagar dari pembungkus OOP di atas C- fungsi gaya. Saya pikir, ketika Anda membaca artikel ini, Anda sudah menebak pilihan saya.

Saat ini, bahasanya lebih dari cocok untukku, dan menulis di kelas bukan masalah besar. Dalam kode mesin, satu instance konsol pada ImGui dilampirkan ke aplikasi yang diluncurkan, di mana objek chaiscript diinisialisasi. Dengan beberapa makro, tugas memperkenalkan kelas baru ke dalam skrip diturunkan ke deskripsi sederhana dari semua metode yang perlu diekspor:

 //      3D-: // rotation CHAI_IMPORT(&GLInstance::RotateX, rotate_x); CHAI_IMPORT(&GLInstance::RotateY, rotate_y); CHAI_IMPORT(&GLInstance::RotateZ, rotate_z); // scale CHAI_IMPORT((GLInstance&(GLInstance::*)(float))&GLInstance::Scale, scale); CHAI_IMPORT((GLInstance&(GLInstance::*)(float, float, float))&GLInstance::Scale, scale); // translation CHAI_IMPORT(&GLInstance::Translate, translate); CHAI_IMPORT(&GLInstance::TranslateX, translate_x); CHAI_IMPORT(&GLInstance::TranslateY, translate_y); CHAI_IMPORT(&GLInstance::TranslateZ, translate_z); // hide / show CHAI_IMPORT(&GLInstance::Hide, hide); CHAI_IMPORT(&GLInstance::Show, show); // getters CHAI_IMPORT(&GLInstance::GetTranslation, translation); CHAI_IMPORT(&GLInstance::GetRotation, rotation); CHAI_IMPORT(&GLInstance::GetScale, scale); 

Dengan cara yang sama, beberapa kelas lagi diekspor, dan kemudian semuanya dihubungkan bersama oleh fungsi lambda yang dideklarasikan langsung dalam kode inisialisasi. Anda dapat melihat hasil skrip di tangkapan layar:

gambar
konsol dari chaiscript ke ImGui: mengunduh dan menginstal objek melalui perintah

Mengingat fleksibilitas perpustakaan secara keseluruhan, mengubah pendekatan untuk mengekspor kelas ke skrip akan hampir secara langsung. Tentu saja, Lua memiliki dokumentasi dan komunitas yang lebih luas, dan bahasa ini akan lebih disukai jika Anda perlu mendapatkan lebih banyak kinerja dari kode skrip (JIT masih melakukan tugasnya), tetapi Anda tetap tidak harus menghapus ChaiScript. Jika Anda memiliki proyek kecil yang membutuhkan skrip, Anda dapat dengan aman bereksperimen dengan alternatif yang tersedia.

Pada catatan ini, saya ingin menyelesaikan artikel ini. Jika Anda sudah memiliki pengalaman bekerja dengan bahasa scripting di dalam C ++ (baik itu Lua atau bahasa lain), di komentar saya akan senang mendengar pendapat Anda tentang ChaiScript dan scripting secara umum. Saya juga menyambut setiap pertanyaan atau komentar mengenai publikasi ini. Terima kasih sudah membaca.

Tautan yang bermanfaat


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


All Articles