Wenn eine Skriptsprache in ein C ++ - Projekt eingebettet werden muss, erinnern sich die meisten Menschen zunächst an Lua. In diesem Artikel wird es nicht darum gehen, über eine andere, nicht weniger bequeme und leicht zu erlernende Sprache namens ChaiScript zu sprechen.

Kurze Einführung
Ich selbst bin aus Versehen auf ChaiScript gestoßen, als ich
eine der Vorlesungen von Jason Turner, einem der Schöpfer der Sprache, sah. Es hat mich interessiert, und in dem Moment, in dem es notwendig war, eine Skriptsprache im Projekt auszuwählen, habe ich mich entschieden - warum nicht ChaiScript ausprobieren? Das Ergebnis hat mich positiv überrascht (meine persönliche Erfahrung wird näher am Ende des Artikels geschrieben), aber egal wie seltsam es klingt, es gab keinen einzigen Artikel im Hub, in dem diese Sprache überhaupt erwähnt wurde, und ich habe das entschieden Es wäre schön, über ihn zu schreiben. Natürlich verfügt die Sprache über eine
Dokumentation und eine
offizielle Website , aber nicht jeder wird sie anhand von Beobachtungen lesen, und das Format des Artikels kommt vielen (einschließlich mir) näher.
Zuerst werden wir über die Syntax der Sprache und all ihre Funktionen sprechen, dann darüber, wie sie in Ihrem C ++ - Projekt implementiert wird, und zum Schluss werde ich ein wenig über meine Erfahrungen sprechen. Wenn ein Teil von Ihnen nicht interessiert ist oder Sie den Artikel in einer anderen Reihenfolge lesen möchten, können Sie das Inhaltsverzeichnis verwenden:
Sprachsyntax
ChaiScript ist in seiner Syntax C ++ und JS sehr ähnlich. Zunächst wird es, wie die meisten Skriptsprachen, dynamisch getippt, hat jedoch im Gegensatz zu JavaScript eine strikte Typisierung (Nr.
1 + "2"
). Es gibt auch einen integrierten Garbage Collector. Die Sprache ist vollständig interpretierbar, sodass Sie Code Zeile für Zeile ausführen können, ohne ihn in Bytecode zu kompilieren. Es unterstützt Ausnahmen (außerdem joint, so dass Sie sie sowohl innerhalb des Skripts als auch in C ++ abfangen können), Lambda-Funktionen und das Überladen von Operatoren. Es reagiert nicht auf Leerzeichen, sodass Sie als einzelne Zeile durch ein Semikolon oder im Python-Stil schreiben und Ausdrücke durch eine neue Zeile trennen können.
Primitive Typen
ChaiScript speichert Integer-Variablen standardmäßig als int, real als double und Strings mit std :: string. Dies geschieht hauptsächlich, um die Kompatibilität mit dem aufrufenden Code sicherzustellen. Die Sprache hat sogar Suffixe für Zahlen, sodass wir explizit angeben können, um welchen Typ es sich bei unserer Variablen handelt:
var myInt = 1
Das Ändern des Variablentyps funktioniert einfach nicht. Wahrscheinlich müssen Sie für diese Typen einen eigenen "=" - Operator definieren. Andernfalls besteht die Gefahr, dass Sie entweder eine Ausnahme auslösen (wir werden später darauf eingehen) oder gerundet werden:
var integer = 3 integer = 5.433 print(integer)
Sie können eine Variable jedoch deklarieren, ohne ihr einen Wert zuzuweisen. In diesem Fall enthält sie eine Art undefiniertes Element, bis ihr ein Wert zugewiesen wird.
Inline-Container
Die Sprache hat zwei Container - Vector und Map. Sie arbeiten sehr ähnlich wie ihre Gegenstücke in C ++ (std :: vector bzw. std :: map), benötigen jedoch keinen Typ, da sie jeden speichern können. Die Indizierung kann wie gewohnt mit ints erfolgen, für Map ist jedoch ein Schlüssel mit einer Zeichenfolge erforderlich. Anscheinend von Python inspiriert, haben die Autoren auch die Möglichkeit hinzugefügt, Container in Code mit der folgenden Syntax schnell zu deklarieren:
var v = [ 1, 2, 3u, 4ll, "16", `+` ]
Beide Klassen wiederholen ihre Entsprechungen in C ++ fast vollständig, mit Ausnahme von Iteratoren, da es stattdessen spezielle Klassen Range und Const_Range gibt. Übrigens werden alle Container als Referenz übergeben, auch wenn Sie die Zuweisung durch = verwenden, was für mich sehr seltsam ist, da bei allen anderen Typen das Kopieren nach Wert erfolgt.
Bedingte Konstruktionen
Fast alle Konstruktionen von Bedingungen und Zyklen lassen sich in einem Beispielcode buchstäblich beschreiben:
var a = 5 var b = -1
Ich denke, die mit C ++ vertrauten Personen haben nichts Neues gefunden. Dies ist nicht verwunderlich, da ChaiScript als leicht zu erlernende Sprache positioniert ist und daher alle bekannten klassischen Designs übernimmt. Die Autoren haben beschlossen, sogar zwei Schlüsselwörter für die Deklaration von Variablen hervorzuheben -
var
und
auto
, falls Sie die Vorteile von auto wirklich mögen.
Ausführungskontext
ChaiScript hat einen lokalen und globalen Kontext. Der Code wird zeilenweise von oben nach unten ausgeführt, kann aber in Funktionen herausgenommen und später (aber nicht früher!) Aufgerufen werden. Innerhalb von Funktionen oder Bedingungen / Schleifen deklarierte Variablen sind von außen standardmäßig nicht sichtbar. Sie können dieses Verhalten jedoch ändern, indem Sie den
global
Bezeichner anstelle von
var
. Globale Variablen unterscheiden sich von gewöhnlichen insofern, als sie erstens außerhalb des lokalen Kontexts sichtbar sind und zweitens neu deklariert werden können (wenn der Wert während der Neudeklaration nicht festgelegt wird, bleibt er gleich).
Wenn Sie eine Variable haben und prüfen müssen, ob ihr ein Wert zugewiesen wurde, verwenden Sie die
is_var_undef
Funktion
is_var_undef
, die true zurückgibt, wenn die Variable nicht definiert ist.
String-Interpolation
to_string()
oder Benutzerobjekte, für die eine
to_string()
-Methode
to_string()
können mit der Syntax
${object}
in eine Zeichenfolge
to_string()
. Dies vermeidet unnötige Verkettungen von Zeichenfolgen und sieht im Allgemeinen viel aufgeräumter aus:
var x = 3 var y = 4
Vector, Map, MapPair und alle Grundelemente unterstützen diese Funktion ebenfalls. Der Vektor wird im Format
[o1, o2, ...]
, Karte als
[<key1, val1>, <key2, val2>, ...]
und MapPair:
<key, val>
angezeigt.
Funktionen und ihre Nuancen
ChaiScript-Funktionen sind Objekte wie alles andere. Sie können erfasst, Variablen zugewiesen, in anderen Funktionen verschachtelt und als Argument übergeben werden. Auch für sie können Sie den Typ der Eingabewerte angeben (was den dynamisch typisierten Sprachen fehlte!). Hierzu müssen Sie den Typ angeben, bevor Sie den Funktionsparameter deklarieren. Wenn der Parameter beim Aufrufen in den angegebenen Wert konvertiert werden kann, erfolgt die Konvertierung gemäß den C ++ - Regeln. Andernfalls wird eine Ausnahme ausgelöst:
def adder(int x, int y) { return x + y } def adder(bool x, bool y) { return x || y } adder(1, 2)
Funktionen in der Sprache können auch über Anrufbedingungen (Call Guard) eingestellt werden. Wird dies nicht beachtet, wird eine Ausnahme ausgelöst, andernfalls wird ein Anruf getätigt. Ich beachte auch, dass, wenn die Funktion am Ende keine return-Anweisung hat, der letzte Ausdruck zurückgegeben wird. Sehr praktisch für kleine Routinen:
def div(x, y) : y != 0 { x / y }
Klassen und Dynamic_Object
ChaiScript hat die Grundlagen von OOP, was ein klares Plus ist, wenn Sie komplexe Objekte bearbeiten müssen. Die Sprache hat einen speziellen Typ - Dynamic_Object. Tatsächlich sind alle Instanzen von Klassen und Namespaces genau Dynamic_Object mit vordefinierten Eigenschaften. Mit einem dynamischen Objekt können Sie ihm während der Ausführung des Skripts Felder hinzufügen und dann darauf zugreifen:
var obj = Dynamic_Object(); obj.x = 3; obj.f = fun(arg) { print(this.x + arg); }
Klassen werden ganz einfach definiert. Sie können auf Felder, Methoden und Konstruktoren gesetzt werden. Ab dem interessanten
set_explicit(object, value)
mit der Sonderfunktion
set_explicit(object, value)
die Felder des Objekts "reparieren", indem Sie das Hinzufügen neuer Methoden oder Attribute nach der Klassendeklaration verbieten (dies erfolgt normalerweise im Konstruktor):
class Widget { var id;
Ein wichtiger Punkt: Tatsächlich sind Klassenmethoden nur Funktionen, deren erstes Argument ein Objekt einer Klasse mit einem explizit angegebenen Typ ist. Daher entspricht der folgende Code dem Hinzufügen einer Methode zu einer vorhandenen Klasse:
def set_id(Widget w, id) { w.id = id } w.set_id(9)
Jeder, der mit C # vertraut ist, kann eine schmerzhafte Erweiterungsmethode ersetzen und ist der Wahrheit sehr nahe. Auf diese Weise können Sie in der Sprache neue Funktionen auch für integrierte Klassen hinzufügen, z. B. für einen String oder eine Ganzzahl. Die Autoren bieten auch eine knifflige Möglichkeit, Operatoren zu überladen: Dazu müssen Sie das Operator-Symbol wie im folgenden Beispiel mit einer Tilde (`) umgeben:
Namespaces
In Bezug auf den Namespace in ChaiScript sollte beachtet werden, dass es sich im Wesentlichen um Klassen handelt, die sich immer in einem globalen Kontext befinden. Sie können sie mit der
namespace(name)
Funktion
namespace(name)
erstellen und anschließend die erforderlichen Funktionen und Klassen hinzufügen. Standardmäßig gibt es keine Bibliotheken in der Sprache. Sie können sie jedoch mithilfe von Erweiterungen installieren, auf die wir später noch eingehen werden. Im Allgemeinen könnte die Namespace-Initialisierung folgendermaßen aussehen:
namespace("math")
Lambda-Ausdrücke und andere Merkmale
Lambda-Ausdrücke in ChaiScript ähneln dem, was wir aus C ++ kennen. Für sie wird das Schlüsselwort
fun verwendet. Außerdem müssen die erfassten Variablen explizit angegeben werden. Dies geschieht jedoch immer anhand von Verweisen. Die Sprache verfügt auch über eine Bindefunktion, mit der Sie Werte an Funktionsparameter binden können:
var func_object = fun(x) { x * x } func_object(9)
Ausnahmen
Während der Skriptausführung können Ausnahmen auftreten. Sie können sowohl in ChaiScript selbst (das wir hier diskutieren) als auch in C ++ abgefangen werden. Die Syntax ist absolut identisch mit Pluszeichen. Sie können sogar eine Zahl oder eine Zeichenfolge ausgeben:
try { eval(x + 1)
Auf eine gute Weise sollten Sie Ihre Klasse von Ausnahmen definieren und sie auslösen. Wir werden im zweiten Abschnitt darüber sprechen, wie man es in C ++ abfängt. Für Interpreter-Ausnahmen gibt ChaiScript Ausnahmen aus, wie eval_error, bad_boxed_cast usw.
Interpreter-Konstanten
Zu meiner Überraschung stellte sich heraus, dass es sich bei der Sprache um eine Art Compiler-Makro handelte - es gibt nur vier davon und alle dienen zur Identifizierung des Kontexts und werden hauptsächlich zur Fehlerbehandlung verwendet:
Fehler beim Überfüllen
Wenn die aufgerufene Funktion nicht deklariert wurde, wird eine Ausnahme ausgelöst. Wenn dies für Sie nicht akzeptabel ist, können Sie eine spezielle Funktion definieren -
method_missing(object, func_name, params)
, die im Fehlerfall mit den entsprechenden Argumenten aufgerufen wird:
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)
Eingebaute Funktionen
ChaiScript definiert viele integrierte Funktionen, und in dem Artikel möchte ich auf besonders nützliche Funktionen eingehen. Darunter:
eval(str)
,
eval_file(filename)
,
to_json(object)
,
from_json(str)
:
var x = 3 var y = 5 var res = eval("x * y")
Implementierung in C ++
Installation
ChaiScript ist eine auf C ++ - Vorlagen basierende Bibliothek, die nur auf Headern basiert. Dementsprechend müssen Sie für die Installation nur ein Klon-
Repository erstellen oder alle Dateien aus
diesem Ordner in Ihr Projekt einfügen. Da dies alles je nach IDE unterschiedlich
#include <chaiscript/chaiscript.hpp>
und in den Foren lange Zeit ausführlich beschrieben wurde, gehen wir davon aus, dass Sie die Verbindung zur Bibliothek hergestellt haben und der Code mit include:
#include <chaiscript/chaiscript.hpp>
kompiliert wurde.
Aufruf von C ++ - Code und Laden von Skripten
Der kleinste Beispielcode, der ChaiScript verwendet, ist wie folgt. Wir definieren eine einfache Funktion in C ++, die std :: string verwendet und den geänderten String zurückgibt, und fügen dann im ChaiScript-Objekt einen Link hinzu, um ihn aufzurufen. Das Kompilieren kann viel Zeit in Anspruch nehmen, ist jedoch in erster Linie darauf zurückzuführen, dass das Instanziieren einer großen Anzahl von Vorlagen für den Compiler nicht einfach ist:
#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")); )"); }
Ich hoffe es ist Ihnen gelungen und Sie haben das Ergebnis der Veranstaltung gesehen. Ich möchte gleich eine Nuance notieren - wenn Sie ein ChaiScript-Objekt als statisch deklarieren, erhalten Sie einen unangenehmen Laufzeitfehler. Dies liegt an der Tatsache, dass die Sprache standardmäßig Multithreading unterstützt und lokale Ablaufvariablen speichert, auf die in ihrem Destruktor zugegriffen wird. Sie werden jedoch zerstört, bevor der Destruktor der statischen Instanz aufgerufen wird. Infolgedessen liegt eine Zugriffsverletzung oder ein Segmentierungsfehler vor. Basierend auf dem
Problem bei Github besteht die einfachste Lösung darin, "
#define CHAISCRIPT_NO_THREADS
in den Compilereinstellungen oder vor dem Einschließen der Bibliotheksdatei zu definieren und dadurch das Multithreading zu deaktivieren. Soweit ich weiß, konnte dieser Fehler nicht behoben werden.
Nun werden wir im Detail analysieren, wie die Interaktion zwischen C ++ und ChaiScript abläuft. Die Bibliothek definiert eine spezielle Schablonenfunktion
fun
, die einen Zeiger auf eine Funktion, einen Funktor oder einen Zeiger auf eine Klassenvariable nehmen und dann ein spezielles Objekt zurückgeben kann, das den Status speichert. Als Beispiel definieren wir die Widget-Klasse in C ++ und versuchen, sie auf verschiedene Arten mit ChaiScript zu verknüpfen:
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);
Wie Sie sehen, arbeitet ChaiScript absolut ruhig mit C ++ - Klassen, die ihm unbekannt sind, und kann deren Methoden aufrufen. Wenn Sie irgendwo im Code einen Fehler machen, wird das Skript höchstwahrscheinlich eine Ausnahme von der Art
error in function dispatch
, die überhaupt nicht kritisch ist. Es können jedoch nicht nur Funktionen importiert werden, sondern es wird auch gezeigt, wie Sie einem Skript mithilfe der Bibliothek eine Variable hinzufügen. Wählen Sie dazu die Aufgabe etwas schwieriger aus - importieren Sie std :: vector <Widget>. Die Funktion
chaiscript::var
und die Methode
add_global
helfen uns dabei. Wir werden auch das öffentliche
Data
zu unserem Widget hinzufügen, um zu sehen, wie das Klassenfeld importiert wird:
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;
Der obige Code zeigt
widget #1 with data: 0
:
widget #1 with data: 0
, widget #2 with data: 2
, widget #3 with data: 4
. Wir haben dem Klassenfeld in ChaiScript einen Zeiger hinzugefügt, und da sich herausstellte, dass das Feld ein primitiver Typ ist, ändern wir seinen Wert. Es wurden auch mehrere Methoden hinzugefügt, um mit
std::vector
, einschließlich
operator[]
. Diejenigen, die mit STL vertraut sind, wissen, dass
std::vector
zwei Indizierungsmethoden hat - eine gibt eine konstante Verknüpfung zurück, die andere eine einfache Verknüpfung. Aus diesem Grund müssen Sie für überladene Funktionen deren Typ explizit angeben, da sonst Mehrdeutigkeiten auftreten und der Compiler einen Fehler auslöst.
Die Bibliothek bietet mehrere weitere Methoden zum Hinzufügen von Objekten, die jedoch fast alle identisch sind. Daher sehe ich keinen Sinn darin, sie im Detail zu betrachten. Als kleinen Hinweis ist hier der folgende Code:
chai.add(chaiscript::var(x), "x");
Verwendung von AWL-Containern
Wenn Sie STL-Container mit
primitiven Typen an ChaiScript übergeben möchten, können Sie Ihrem Skript eine Instanziierung für Vorlagencontainer hinzufügen, damit Sie nicht für jeden Typ Methoden importieren.
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, complex, matrix) . ChaiScript
type_conversion
. Complex int double :
class Complex { public: float Re, Im; Complex(float re, float im = 0.0f) : Re(re), Im(im) { } }; int main() { chaiscript::ChaiScript chai;
, C++, ChaiScript. , . ,
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, access violation , .
, 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 , .
math 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 }
. , math . , C++ , , .
3D- OpenGL , , .
, , , « », .
, ChaiScript , Lua. , , : , C++ C, - C-style . , , .
, . ImGui, chaiscript. , :
, -, . :
chaiscript ImGui:, . , Lua , , (JIT ), ChaiScript . , , .
. C++ ( Lua ), ChaiScript . . .