C++ scripting alternatives: easy-to-bind-in-C++ scripting

German Diago Gomez
ITNEXT
Published in
10 min readMay 24, 2021

Binding ChaiScript and Wren into a small game

Photo by Casparrubin on Unsplash

Hello everyone,

Since I have been trying some scripting languages back and forth for scripting a small game and I accumulated some experience, I would like to share here my experience with the hope that someone could find it useful.

This article is focused on Scripting APIs that are reasonably easy to bind with C++, whether natively or with a helper library.

I tried and tested exposing C++APIs to foreign languages for the purpose of scripting a small cards game.

Why scripting?

For people unfamiliar with scripting: scripting is usually used to accelerate the development of your own programs.

It can very effective because, besides eliminating the (explicit) compile step, you can code the logic in them with a simpler-to-use language.

With this in mind, scripting can accelerate your edit-compile-debug iterations by a lot by eliminating the compile step. Think of any change you do to your code + recompiling but eliminating the compile step. Things become almost instantaneous.

The game

The game is a networked 4-player game, where each player takes the turn to play one of the cards in their hand and cards are played until the deck is fully consumed.

It is a game called Guiñote that is popular in some areas of Spain. It has many variations like Brisca, Tute and others, though this is not terribly important for what I am going to explain in the article.

The purpose of the article is to share my experience binding a C++ API and exposing it into scripting languages.

Disclaimer: keep in mind that I am not an expert in the binding APIs therefore there could be some knowledge that escapes to me.

Relevant points about the engine architecture (for binding purposes)

The game is composed of a controller that loads screens. Each screen is represented by a script that controls the screen logic.

The script uses game views and game models, besides some networking APIs (which were written via Capnproto, stay tuned if I find myself with the strength to write an article about it!)

The C++ side of the game goes into a screen via a controller, and, at that time, it loads the screen and gives control to the scripting engine.

A virtual machine is created just before a screen is loaded and from there on the scripting is responsible for the processing.

Once the script is finished, the script returns a couple of strings:

  1. the next screen to load
  2. the arguments for the next screen (equivalent to argv in main in C++)

Control is returned to the C++ side after returning. In order to make the scripts functional, some additional data is exposed via the environment as a global: basically the runtime configuration and logger, which are available to the script engine.

When a screen finishes, the virtual machine is destroyed. This way no state is kept alive between screen runs. This also makes easier my debugging, etc.

Basically I wanted to accelerate my code logic writing for screens.

From the scripting side of things this means:

  1. Being able to load scripts at run-time from a file (when developing)
  2. Being able to load scripts from memory (when embedding the code in the final binary)
  3. Being able to pass arguments to the scripting main function.
  4. C++ types must be constructed/destroyed on the scripting side, besides used
  5. Global, already-existing data in the C++ side must be exposed to the scripting environment.

Think of one of these scripts as a program by itself that can get input arguments and has some environment (log, config, for example). It is like main in C++, Java, C#, etc. It does something, and then it returns information to the controller so that the controller knows what screen to load next.

Scripting candidates

I wanted to choose a scripting language that fulfills the following as much as possible:

  1. Easy to bind to C++. I do not want to fight the integration all the time and waste a lot of time getting it correct. This is probably one of the most important points.
  2. Support for lightweight concurrency is a very big plus, almost as important as the ease of binding.
  3. It cannot be Python, since, according to my information, it could be difficult to port to Web Assembly down the road, so I discarded it directly.
  4. Familiar: it should not look too weird compared to standard practice: something like javascript-ey, C-like or pythonic is ok. Besides the syntax, semantics should not be weird as well.
  5. Prefer dynamic to static typing, since static typing can remove the coding speed: it makes you think about types when typing and refactoring can become more rigid.
  6. Binding callbacks from the language from an exposed C++ function is a nice plus. If it can work out of the box, great. Otherwise, it should be reasonably easy to accomplish.
  7. Performance is not important. I do not need it.

Languages considered

  • Lua + Sol2. It was discarded because of 3. It has unfamiliar syntax, but worse, unfamiliar semantics: no classes, use tables, start indexing at 1 and other oddities, just as being able to call functions with the wrong number of arguments and returning nil on the way. Also, use tables for both hash tables and arrays. It is powerful, do not misunderstand me, and Lua supports good concurrency. It was just not what I was looking for because of the mentioned things.
  • Squirrel. The language looked really nice. But it was discarded because of 1. and the projects that support nicer bindings looked incomplete. It supports good concurrency and it looks a lot like a hybrid between C++/Lua. But way closer to C++ (class-based)
  • AngelScript. This one is statically typed, but I really could not figure out how to bind it without being intrusive.
  • ChaiScript. From all the list, this is the only one designed to be bound with C++. This lowers the barrier to create bindings. On that side, it delivers, although there are very good C++ bindings to originally only-C scripting languages that get close to ChaiScript. It is a javascript-flavored scripting language, I would say, but with its own things. The downsides are… keep reading.
  • Wren + wrenbind17. From all the list, I think this one and Squirrel, scripting-wise, are the closest to my ideal, since both support good concurrency and do look very natural and unsurprising to use. Also, Wren documentation is not big but quite good, IMHO. I could find the APIs supported in the scripting language easily and understand how features work. Credits to wrenbind17, it made viable for me to try binding C++. It would not have been possible without it.

From that list, I finally went, first, with ChaiScript, because it was so easy to bind.

Features overview

ChaiScript

Chaiscript has (roughly) the following features:

  • Dead-easy to bind out of the box, as advertised. ChaiScript will be smart enough to know when you are instantiating a derived class of some base class and even figure out how to invoke derived-only methods, as long as you tell ChaiScript the relationship. The basic API is to call chai.add(fun|const_var|base_cass…). If you call a function the same as your class, that becomes a constructor. You do not need to register your copy constructor as long as you do not want to clone things. You can also use lambdas or free functions and register as instance methods of a class as long as the signature matches.
Exposing C++ types in ChaiScript
  • free functions (type is optional and it will be checked):
Free Functions in ChaiScript
  • functions with guards
Function guards in ChaiScript
  • Exception handling that integrates with C++ exception handling: catch your C++ exceptions in ChaiScript! Even your own types not derived from exceptions can be caught.
Exception handling in ChaiScript
  • classes support data and functions, but not static functions (for that there is free functions!)
Class example in ChaiScript. No static functions allowed since free functions can be used.
  • classes are very dynamic, unlike Wren. It supports dynamic dispatch of non-existing functions via method_missing (a-la-Ruby). This class will get some Json string and make it accessible via myjson.varx.vary.varz. Not terribly efficient but it works and it makes the point about dynamic object access of non-existing functions:
Dynamic Json class implementation in pure ChaiScript. You can access Json fields via method_missing delegation
  • Your own vector types can be exposed (std::vector<MyType> instantiations). I think the same holds true for Wren, but I did not need it so far.
  • Function callbacks via std::function<>!! Honestly, this one is quite unique among scripting bindings in C++ as far as I know of, and it just works in ChaiScript. This means that you can have this:
Exposing a function with a std::function parameter in C++ to ChaiScript accepts Chaiscript lambdas and functions
  • The language control flow supports range-based for loop, while, if and similar familiar stuff from C++

Wren

Wren supports classes, static functions, unsurprising control flow and nice concurrency via stackful coroutines and has a clear iterator protocol.

It also supports lists and dictionaries with easy-to-use syntax. You can overload operators from C++ side and in the language, as in ChaiScript. It supports ranges and filtering, transforming, etc. via sequences. I found it a bit more polished than ChaiScript.

Simple Wren class example

Below I would like to focus on the differences between Wren and ChaiScript.

  1. Wren is more class-based in the sense that you can have free functions via variables as well, but you have static functions for that. As far as I know, free functions are not possible to expose from C++.
  2. Inheritance does not work out-of-the-box in Wren for derived classes when you return a base class, though there is a documented trick here
  3. To my taste, modularity and how to import is nicer in Wren, though I am not sure if they are equivalent.
  4. It supports overloading by arity, but not function guards a-la ChaiScript. I am not sure if ChaiScript supports overloading.
  5. Wren supports full coroutines via fibers.
  6. The original way of binding Wren is via a C API. It is fast (at runtime) and so on, but a hell to use: what made bindings viable for me was wrenbind17
  7. In Wren you cannot use std::function<> from C++ directly, . This forces to use some wrapper classes in C++.
  8. Exposing global variables (Env in the script below is a global) is not as straightforward as it could be. In order to set a global variable I used a trick emitting code and getting a class with a static function set from pure Wren code that would expose a C++-side variable.
  9. In Wren you can only expose a constructor. This is not usually a problem. However, you cannot expose a constructor of a non-copyable type. It will fail. The workaround is to create a C++ function that returns a std::shared_ptr<YourType> and expose it as a static function in Wren.
  10. Exception handling is not integrated like in ChaiScript
  11. Wren classes are not as dynamic (but have faster lookup): the methods/fields are determined when the code is loaded.

A taste of Wren below. I think the code is self-explanatory

Game Menu Screen in Wren

This is how I exposed C++-side state, namely, a class instance, on the Wren side. It took me a while to figure out a way without using static variables. This did the trick:

Exposing existing state in Wren from C++ via a trick

Gotchas in ChaiScript and Wren

  • In Wren you need a copy-constructible type to expose via a constructor. There is a workaround, though, explained in this article.
  • In Wren, exposing state from C++ to Wren was not immediately obvious (also documented in this article)
  • In ChaiScript, once you set the type of variable if you try to assign a different unrelated type, it will throw. But you can set an undefined one for the first time. This fails:
auto vec = []
vec.push_back(MyClass()) // Works
vec[0] = 5 // Fails
var aVar // is_var_undefined() == true
aVar = Mytype() // It works, now aVar has MyType()
aVar = 3 // Fails
  • In ChaiScript top-level variables with or without global behave differently. Careful with captures in lambdas, for example. Globals do not need one.
  • In ChaiScript C++-exposed variables and classes created in ChaiScript behave slightly different. For example:
v = CppExposedClass() //Call copy constructor
w = PureChaiClass() // no copy constructor called!
v := CppExposedClass() // No copy constructor called, used :=
  • In ChaiScript, capture by-value vs reference is not too obvious. Sometimes it seemed to try to copy, other times it did not. Sometimes I ended up doing things like:
// Uses reference semantics for C++-side classes. In ChaiScript// classes you always avoid the copy constructor
var v := something
// These two are equivalent:
var & v = something
auto & v = something
var lmbda = [v]() { ...} // No copy in capture for sure
  • In Wren you can create free functions, but there is no syntax that is non-variable style AFAIK, and you cannot invoke via () directly:
var f = Fn.new {
....
}
// This fails, you cannot call itf()//Use explicit syntax
f.call()

Development workflow

Both Wren and ChaiScript do what they advertise. Though I found a few fundamental differences that lean, in my humble opinion, strongly in favor of Wren.

  1. In ChaiScript, if you get an error, you do not get, at least by default, the location of the error. This makes debugging very difficult to the point that it can drain your productivity.
  2. On the other hand, exposing code from ChaiScript is a little easier and exceptions are integrated.
  3. In Wren the location of errors is clear immediately.
  4. The concurrency in Wren is really nice.

Conclusion

I think both are capable tools, but what made the difference is 3. and 4, compared to 1. in ChaiScript. To me, this ruins the experience quite a bit.

In ChaiScript it is difficult to follow where the code crashes, so I had to populate all parts with log messages. In Wren I did not need it at all.

Also, the lack of concurrency in ChaiScript made the code full of callbacks and I had to spawn tasks in C++-side without support for coroutines. This made the code flow way less natural, inverted and callback-based. It also created some problems since I had to dispose the callback code or it would crash, I recall. But I am not certain about it right now.

In Wren, if you see the code listing, you will see that it looks natural and easy to follow. That code corresponds to the Game Menu screen.

Provided that ChaiScript fixed the line number error, I would still choose Wren if I was going to take advantage of concurrency, as it was my case.

If you need a lot of code with callbacks, though, ChaiScript could save you some boilerplate out of the box since it does not need callback wrapping, it is fully integrated via std::function<>. Also, exceptions are supported in ChaiScript.

All in all, I could recommend both provided that ChaiScript fixed the line number location and you are not going to take advantage of the concurrency. Also, for what I heard so far, if speed is a concern, ChaiScript should be quite slower compared to Wren.

But given the state of things and being realistic, I think Wren is more ready for production than ChaiScript: it is fast (if that is a concern) vs ChaiScript that instead of bytecode it uses pure interpretation, it supports concurrency and it shows where errors happen.

Thanks for reading!

Published in ITNEXT

ITNEXT is a platform for IT developers & software engineers to share knowledge, connect, collaborate, learn and experience next-gen technologies.

Written by German Diago Gomez

In software industry for more than a decade doing backend software.

No responses yet

What are your thoughts?