While working with ChaiScript for FyS Online, I didn’t meet big issue while implementing it, as explained in this post, one of the keypoint of ChaiScript is how easy it is to bind it with C++ code. However, I found out some interesting features of ChaiScript that are unfortunately not very well documented. Most of the documentation of ChaiScript is based on the examples and the cheatsheet provided on the website of ChaiScript.

But the ChaiScript community on discourse is quite important, and some answers to their question end up revealing “hidden” features or tricks that are very useful when implementing.

This blog post has for purpose to list the one I found relevant (and giving the source).

Register a class in ChaiScript with add_class

This is one of the utility function that you can find in the test cases of chaiscript. This function is by far one of the most useful utility function provided for ChaiScript. It gives you an easy way to register a C++ class to the ChaiScript engine in one instruction, giving the constructor and the functions that it contains. You can find below examples of how it works.

Register normal class

A simple example from chai GitHub on how to register a class:

// c++ class to register class Utility_Test { public: void function() {} std::string function2() { return "Function2"; } void function3() {} std::string functionOverload(double) { return "double"; } std::string functionOverload(int) { return "int"; } }; // // C++ Code to register Utility_Test class chaiscript::ModulePtr m = chaiscript::ModulePtr(new chaiscript::Module()); using namespace chaiscript; // in order to not use chaiscript::fun chaiscript::utility::add_class<Utility_Test>(*m, "Utility_Test", { constructor<Utility_Test ()>(), constructor<Utility_Test (const Utility_Test &)>() }, { {fun(&Utility_Test::function), "function"}, {fun(&Utility_Test::function2), "function2"}, {fun(&Utility_Test::function3), "function3"}, {fun(static_cast<std::string(Utility_Test::*)(double)>(&Utility_Test::functionOverload)), "functionOverload" }, {fun(static_cast<std::string(Utility_Test::*)(int)>(&Utility_Test::functionOverload)), "functionOverload" }, {fun(static_cast<Utility_Test & (Utility_Test::*)(const Utility_Test &)>(&Utility_Test::operator=)), "=" } } ); 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 // c++ class to register class Utility_Test { public : void function ( ) { } std :: string function2 ( ) { return "Function2" ; } void function3 ( ) { } std :: string functionOverload ( double ) { return "double" ; } std :: string functionOverload ( int ) { return "int" ; } } ; // // C++ Code to register Utility_Test class chaiscript :: ModulePtr m = chaiscript :: ModulePtr ( new chaiscript :: Module ( ) ) ; using namespace chaiscript ; // in order to not use chaiscript::fun chaiscript :: utility :: add_class < Utility_Test > ( * m , "Utility_Test" , { constructor < Utility_Test ( ) > ( ) , constructor < Utility_Test ( const Utility_Test & ) > ( ) } , { { fun ( &Utility_Test :: function ) , "function" } , { fun ( &Utility_Test :: function2 ) , "function2" } , { fun ( &Utility_Test :: function3 ) , "function3" } , { fun ( static_cast < std :: string ( Utility_Test :: * ) ( double ) > ( &Utility_Test :: functionOverload ) ) , "functionOverload" } , { fun ( static_cast < std :: string ( Utility_Test :: * ) ( int ) > ( &Utility_Test :: functionOverload ) ) , "functionOverload" } , { fun ( static_cast < Utility_Test & ( Utility_Test :: * ) ( const Utility_Test & ) > ( &Utility_Test :: operator = ) ) , "=" } } ) ;

Register struct

It is possible to register plain old object (struct) via this utility function (in this case, each public member are actually taken as function by the add_class):

// C++ struct to register struct Life { int current; int total; bool isDead; }; // // C++ code to register Life struct chaiscript::ModulePtr m = chaiscript::ModulePtr(new chaiscript::Module()); using namespace chaiscript; chaiscript::utility::add_class<Life>( *m, "Life", {}, { {fun(&Life::current), "current"}, {fun(&Life::total), "total"}, {fun(&Life::isDead), "isDead"} } ); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // C++ struct to register struct Life { int current ; int total ; bool isDead ; } ; // // C++ code to register Life struct chaiscript :: ModulePtr m = chaiscript :: ModulePtr ( new chaiscript :: Module ( ) ) ; using namespace chaiscript ; chaiscript :: utility :: add_class < Life > ( * m , "Life" , { } , { { fun ( &Life :: current ) , "current" } , { fun ( &Life :: total ) , "total" } , { fun ( &Life :: isDead ) , "isDead" } } ) ;

Register enum

It is also possible to register enum via this utility function:

// C++ Enum to register enum Utility_Test_Numbers { ONE, TWO, THREE }; // // C++ Code to register Utility_Test_Numbers chaiscript::ModulePtr m = chaiscript::ModulePtr(new chaiscript::Module()); using namespace chaiscript; chaiscript::utility::add_class<Utility_Test_Numbers>(*m, "Utility_Test_Numbers", { { ONE, "ONE" }, { TWO, "TWO" }, { THREE, "THREE" } } ); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // C++ Enum to register enum Utility_Test_Numbers { ONE , TWO , THREE } ; // // C++ Code to register Utility_Test_Numbers chaiscript :: ModulePtr m = chaiscript :: ModulePtr ( new chaiscript :: Module ( ) ) ; using namespace chaiscript ; chaiscript :: utility :: add_class < Utility_Test_Numbers > ( * m , "Utility_Test_Numbers" , { { ONE , "ONE" } , { TWO , "TWO" } , { THREE , "THREE" } } ) ;

Lambda capture

ChaiScript syntax is very similar to C++, it goes also for chai lambdas that can capture variable as the following example provided by Jason shows:

def f(v) { print(v); } // lambda with capture, similar to C++, // but the capture is by-reference here set_func(fun[a](){ f(a); }) 1 2 3 4 5 6 7 def f ( v ) { print ( v ) ; } // lambda with capture, similar to C++, // but the capture is by-reference here set_func ( fun [ a ] ( ) { f ( a ) ; } )

It is important to note that the capture is made by reference and not copy in chai.

The return key-word

This is an interesting point that I discovered while debugging scripts. ChaiScript is returning values by throwing this value as an exception (which can make debug with a debugger quite complicated).

It is a design decision Jason Turner regret doing and could imply performance loss in some cases. It is good to remember that the last instruction made by a chai script, is the value returned in the eval (and this without exception mechanism).

Avoiding the return key-word when possible is certainly a good practice in ChaiScript.

Helper functions

Chaiscript engine has a full set of useful function, that is unfortunately hardly documented. A way to display all the function registered by chai engine is to evaluate the function dump_system in chai. Some of those functions implementation can be found on the prelude GitHub file (containing the implementation of some helper functions, with some comments).

chai.eval("dump_system()"); 1 chai . eval ( "dump_system()" ) ;

The number of functions is quite important (pastebin result), In this section, I am going to explain the one I find the most interesting below:

String conversion

It is possible to do string conversion to int/double/char etc… thanks to the chaiscript function below (list non-exhaustive as you also can convert to int_64 or others, check the pastebin result for more string conversion).

// In chaiscript var i = to_int("42") var i = to_unsigned_int("42") var i = to_double("42.42") var i = to_long("42") var i = to_long_long("42") var i = to_unsigned_long("42") var i = to_unsigned_long_long("42") var i = to_size_t("42") var i = to_char("4") to_string(x) // x can be any of the above type and more // to_string as the name say will convert a number (or other) to a string 1 2 3 4 5 6 7 8 9 10 11 12 13 // In chaiscript var i = to_int ( "42" ) var i = to_unsigned_int ( "42" ) var i = to_double ( "42.42" ) var i = to_long ( "42" ) var i = to_long_long ( "42" ) var i = to_unsigned_long ( "42" ) var i = to_unsigned_long_long ( "42" ) var i = to_size_t ( "42" ) var i = to_char ( "4" ) to_string ( x ) // x can be any of the above type and more // to_string as the name say will convert a number (or other) to a string

function_exists / call_exists

This chaiscript function makes you able to check if a function exist, or if a specific object has a method. This is useful in order to detect if you are working with a container (for function guards, see below).

// in chaiscript var container = [1, 2, 3, 4, 5] var notContainer = 42 var isContainer = call_exists(range, notContainer) // false var isContainer = call_exists(range, container) // true 1 2 3 4 5 6 // in chaiscript var container = [ 1 , 2 , 3 , 4 , 5 ] var notContainer = 42 var isContainer = call_exists ( range , notContainer ) // false var isContainer = call_exists ( range , container ) // true

Function guards

This one is clearly documented in the chaiscript example. It’s a feature giving a restriction on when one overload of the function is called instead of another. But it is an important feature that can be very useful when combined with call_exist.

// in chaiscript // function definition with function guards def myFunc(x) : x > 2 && x < 5 { print(to_string(x) + " is between 2 and 5") } def myFunc(x) : x >= 5 { print(to_string(x) + " is greater than or equal to 5") } def myFunc(x) : call_exists(range, x) { print("myFunc has been called with a container") } myFunc([0,2,3]) // Will print "myFunc has been called with a container" myFunc(3) // Will print "3 is between 2 and 5" myFunc(42) // Will print "42 is greater than or equal to 5" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // in chaiscript // function definition with function guards def myFunc ( x ) : x > 2 && x < 5 { print ( to_string ( x ) + " is between 2 and 5" ) } def myFunc ( x ) : x >= 5 { print ( to_string ( x ) + " is greater than or equal to 5" ) } def myFunc ( x ) : call_exists ( range , x ) { print ( "myFunc has been called with a container" ) } myFunc ( [ 0 , 2 , 3 ] ) // Will print "myFunc has been called with a container" myFunc ( 3 ) // Will print "3 is between 2 and 5" myFunc ( 42 ) // Will print "42 is greater than or equal to 5"

Good to know

Below a non-exhaustive list of self-explanatory functions available in chaiscript engine (see: dump_system result), more interesting function exist (algorithm function like any_of, all_of and such) in chaiscript engine.

Object max(Object, Object) Object min(Object, Object) Object odd(Object) // return 0 or 1 Object even(Object) // return 0 or 1 Object from_json(const string) string to_json(const Object) // return sum/product of elements in the container (parameter) Object sum(Object) Object product(Object) // chaiscript -> ( def reverse(container) ) // Returns the reverse of the given container (parameter) Object reverse(Object) // chaiscript -> ( def join(container, delim) ) // Returns a string of the elements in container delimited by the second value string Object join(Object, Object) // chaiscript -> ( def filter(container, f) ) // Returns a new Vector which match the second value function (second parameter) on element of container (first parameter) Object filter(Object, Object) // triming string ltrim() rtrim() trim() // usage example: stringObject = " de kf j eigej e " stringObject.trim() print stringObject // will print "de kf j eigej e" // ... Not exaustive 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 Object max ( Object , Object ) Object min ( Object , Object ) Object odd ( Object ) // return 0 or 1 Object even ( Object ) // return 0 or 1 Object from_json ( const string ) string to_json ( const Object ) // return sum/product of elements in the container (parameter) Object sum ( Object ) Object product ( Object ) // chaiscript -> ( def reverse(container) ) // Returns the reverse of the given container (parameter) Object reverse ( Object ) // chaiscript -> ( def join(container, delim) ) // Returns a string of the elements in container delimited by the second value string Object join ( Object , Object ) // chaiscript -> ( def filter(container, f) ) // Returns a new Vector which match the second value function (second parameter) on element of container (first parameter) Object filter ( Object , Object ) // triming string ltrim ( ) rtrim ( ) trim ( ) // usage example: stringObject = " de kf j eigej e " stringObject . trim ( ) print stringObject // will print "de kf j eigej e" // ... Not exaustive

It is possible to call a chaiscript method (with chaiscript object parameters) from the C++ code, to do so you need to retrieve the chaiscript object you want to send as a parameter from chai engine. And then you can retrieve the chai function and call it with the previously retrieved chai object (see example below from Jason) :

// Get object from engine auto obj = script.eval("t"); // Get typed std::function wrapper of method auto func = script.eval<std::function<std::string(Boxed_Value &, const std::string &)>>("func"); // Execute function auto result = func(obj, "some string"); // result should be of type string and equal to the return value of `func` now 1 2 3 4 5 6 7 // Get object from engine auto obj = script . eval ( "t" ) ; // Get typed std::function wrapper of method auto func = script . eval < std :: function < std :: string ( Boxed _ Value & , const std :: string & ) >> ( "func" ) ; // Execute function auto result = func ( obj , "some string" ) ; // result should be of type string and equal to the return value of `func` now