ZetScript is a JavaScript-like programming language featuring a virtual machine, dynamic garbage collector, and an API for seamless integration with C++ code, providing a straightforward approach for binding and exposing functionalities.
The source code is already available on github.
Other Links
Introduction
ZetScript is a programming language with an API that allows to bind C++ code in script side. ZetScript includes the following features:
- Virtual Machine
- Script Language close to JavaScript
- Dynamic Garbage collector
- Straightforward way to expose C++ code in ZetScript and bind ZetScript code to C++
- Implement operators on types and member properties through metamethods
A helloworld
is shown in the following code:
#include "zetscript.h"
int main(){
zetscript::ScriptEngine script_engine;
script_engine.compileAndRun("Console::outln(\"Hello World from ZetScript!\")");
return 0;
}
List 1.1
The code presented in list 1.1 instances script engine and compiles and runs a line of code that prints Hello World from ZetScript
using builtin Console::outln
.
Build ZetScript
The compilation and execution of ZetScript has been tested in the following platforms:
- Linux through gcc toolchain >= 4.8
- MacOS through clang toolchain
- Windows through Mingw >= 4.8 or VisualStudio 2015,2017/2019
- Download and decompress ZetScript project in some directory.
- Open a terminal in Linux or MacOs or in Windows, open a command prompt terminal (For Windows using Visual Studio, open the terminal using `Developer Command Prompt` )
- Go to the ZetScript project directory which was decompressed and create building information through
cmake
with the following command:
cmake -Bbuild
Optionally, you can pass the following options in cmake
parameters:
- In Linux, MacOS and MingW environments compile the project with the following command:
make -C build
For Windows using Visual Studio, compile the project with the following command:
msbuild build\zetscript.sln /property:Configuration=Release
After the build, ZetScript library and the command line tool will be placed at bin directory.
Yet Another Script Language?
In 2016, I decided to start work with scripting because I needed to be faster in terms of production. Of course, because I always try to be a good engineer, I started to play with all script engines that are already in this world.
Some of them were faster, but it I didn't like its syntax. Others, the syntaxes were good but slow at run-time, etc. After spending a lot of time playing with them and trying to decide what kind of script engine could be a better candidate for my project, I finished by knowing that none of them fitted at all for my needs.
So at this date, I decided to start my own script engine, but apart from putting my effort with my requirements, I read from some forums what it expects to have from a good script engine. I combined both things together and it became ZetScript.
I've decided to make ZetScript public because I like to make life easy for people, just in case they find this tool useful.
Language Overview
ZetScript Types
ZetScript has defined types as integers, floats, booleans, arrays and objects.
var i=10;
var f=0.5;
var s="a string";
var b=true;
var array=[1,0.5, "a string", true];
var object={
i: 10,
f: 0.5,
s: "a string",
b: true,
v: [1,0.5, "a string", true]
};
Scope
ZetScript has an easy concept of global scope declaring variables at the top of evaluating scope. Local variables are within blocks like function or loops. Local var
s are destroyed when it exits from block, unless it is not referenced by other variable.
var i= 0;
{
var j= 2;
}
Conditionals
ZetScript supports if
-else
and switch
conditionals:
var number=5;
if(number < 10){
Console::outln("number less than 10");
}else{
Console::outln("number greater equal than 10");
}
switch(number){
case 0:
case 1:
Console::outln("number is 0 or 1");
break;
case 2:
Console::outln("number is 2");
break;
default:
Console::outln("number is : "+number);
break;
}
Loops
ZetScript supports while
, do
-while
and for
as loop iterators:
var i=0;
while(i < 10){
Console::outln("i:"+i);
i++;
}
do{
Console::outln("i:"+i);
i++;
}while(i < 20);
for(var j=0; j < 10; j++){
Console::outln("j:"+i);
}
var a=[1,2,3,4];
for(var k,v in a){
Console::outln("v["+k+"]:"+v);
}
Class
ZetScript supports custom class implementation with constants, properties, metamethods and inheritance support. Also, it can implement a member function after class declaration.
var n_entity=1;
class Entity{
var __id__=0;
const MAX_ENTITIES=10;
static isEntityDead(_entity){
return _entity.health==0;
}
static _nequ(_e1,_e2){
return _e1.id!=_e2.id;
}
constructor(_name="unknown",_health=0){
this.name=_name
this.__id__=n_entity++;
this.setHealth(_health);
}
update(){
Console::outln("From Entity")
}
id{
_get(){
return this.__id__;
}
}
}
function Entity::setHealth(_health){
this.health = _health;
}
class Player extends Entity{
constructor(){
super("Player",10);
}
update(){
super();
Console::outln("From player")
}
}
var p=new Player();
var e=new Entity();
Console::outln("Entity::MAX_ENTITIES: {0}",Entity::MAX_ENTITIES)
Console::outln("p.id: {0} p.name: {1} p.health: {2} Entity::isEntityDead(p): {3}",
p.id,p.name,p.health,Entity::isEntityDead(p))
p.update();
if(p!=e){
Console::outln("'p' and 'e' are NOT equals")
}
API Overview
Call C++ Code from ZetScript
To call C++ code from ZetScript is done by defining and registering a C function.
#include "zetscript.h"
void sayHelloWorld(zetscript::ScriptEngine *_script_engine){
printf("Hello world\n");
}
int main(){
zetscript::ScriptEngine script_engine;
script_engine.registerFunction("sayHelloWorld",sayHelloWorld);
script_engine.compileAndRun(
"sayHelloWorld();"
);
return 0;
}
Call ZetScript from C++
To call ZetScript from C++ code is done by binding script function after compile script function.
#include "zetscript.h"
int main(){
zetscript::ScriptEngine script_engine;
script_engine.compile(
"function sayHello(){\n"
" Console::outln(\"call from 'sayHello'!\")\n"
"}\n"
);
auto say_hello=script_engine.bindScriptFunction<void ()>("sayHello");
say_hello();
return 0;
}
Exposing C++ Types to ZetScript
To expose C++ type to ZetScript is done by registering C++ type. To expose members functions or variables is done by defining and registering a C function. The following example shows an example of registering class EntityNative
.
#include "zetscript.h"
int n_entity=1;
class EntityNative{
public:
const static int MAX_ENTITIES=10;
int __id__=0;
int health=0;
std::string name="entity";
};
EntityNative *EntityNative_new(
zetscript::ScriptEngine *_script_engine
){
EntityNative *entity=new EntityNative;
entity->__id__=n_entity++;
entity->name="entity_"+std::to_string(entity->__id__);
return entity;
}
void EntityNative_delete(
zetscript::ScriptEngine *_script_engine
,EntityNative *_this
){
delete _this;
}
void EntityNative_constructor(
zetscript::ScriptEngine *_script_engine
,EntityNative *_entity
,zetscript::String *_name
,zetscript::zs_int _health
){
_entity->name=_name->toConstChar();
_entity->health=_health;
}
zetscript::zs_int EntityNative_MAX_ENTITIES(
zetscript::ScriptEngine *_script_engine
){
return EntityNative::MAX_ENTITIES;
}
bool EntityNative_isEntityDead(
zetscript::ScriptEngine *_script_engine
,EntityNative *_entity
){
return _entity->health==0;
}
bool EntityNative_nequ(
zetscript::ScriptEngine *_script_engine
,EntityNative *_e1
,EntityNative *_e2
){
return _e1->__id__!=_e2->__id__;
}
void EntityNative_update(
zetscript::ScriptEngine *_script_engine
,EntityNative *_this
){
printf("Update from EntityNative\n");
}
zetscript::zs_int EntityNative_id_get(
zetscript::ScriptEngine *_script_engine
,EntityNative *_this
){
return _this->__id__;
}
zetscript::String EntityNative_name_get(
zetscript::ScriptEngine *_script_engine
,EntityNative *_this
){
return _this->name.c_str();
}
int main(){
zetscript::ScriptEngine script_engine;
script_engine.registerType<EntityNative>
("EntityNative",EntityNative_new,EntityNative_delete);
script_engine.registerConstructor<EntityNative>(EntityNative_constructor);
script_engine.registerConstMemberProperty<EntityNative>
("MAX_ENTITIES",EntityNative_MAX_ENTITIES);
script_engine.registerStaticMemberFunction<EntityNative>
("isEntityDead",EntityNative_isEntityDead);
script_engine.registerStaticMemberFunction<EntityNative>("_nequ",EntityNative_nequ);
script_engine.registerMemberFunction<EntityNative>("update",EntityNative_update);
script_engine.registerMemberPropertyMetamethod<EntityNative>
("id","_get",EntityNative_id_get);
script_engine.registerMemberPropertyMetamethod<EntityNative>
("name","_get",EntityNative_name_get);
script_engine.compileAndRun(
"var e1=new EntityNative();\n"
"var e2=new EntityNative();\n"
"Console::outln(\"EntityNative::MAX_ENTITIES: {0}\",
EntityNative::MAX_ENTITIES);\n"
"Console::outln(\"e1.id: {0} e1.name: {1} \",e1.id,e1.name);\n"
"Console::outln(\"e2.id: {0} e2.name: {1} \",e2.id,e2.name);\n"
"if(e1!=e2){\n"
" Console::outln(\"'e1' and 'e2' are NOT equals\")\n"
"}\n"
"e1.update();\n"
);
return 0;
}
Finally, another interesting feature is that ZetScript can extend script class from C registered class. The following example replaces the previous script with the following one that extends Player
from EntityNative
.
script_engine.compileAndRun(
"// Extends player from registered EntityNative\n"
"class Player extends EntityNative{\n"
" constructor(){\n"
" super(\"Player\",10);\n"
" }\n"
"\n"
" // Override EntityNative::update\n"
" update(){\n"
" // Calls EntityNative::update\n"
" super();\n"
" Console::outln(\"From player\");\n"
" }\n"
"}\n"
"\n"
"var p=new Player();\n"
"var e=new EntityNative();\n"
"\n"
"Console::outln(\"EntityNative::MAX_ENTITIES: {0}\",EntityNative::MAX_ENTITIES)\n"
"Console::outln(\"p.id: {0} p.name: {1} \",p.id,p.name)\n"
"\n"
"if(p!=e){\n"
" Console::outln(\"'p' and 'e' are NOT equals\")\n"
"}\n"
"p.update();\n"
"\n"
);
Metamethods
ZetScript supports the following implementation of metamethods to perform operations through objects.
Member metamethods
_addassign
: Implements addition assignment operator (aka += ) with a value entered by parameter as right operand _andassign
: Implements bitwise AND assignment operator (aka &= ) with a value entered by parameter as right operand. _divassign
: Implements division assignment operator (aka /= ) with a value entered by parameter as right operand. _in
: Implements in operator _modassign
: Implements modulus assignment operator (aka %= ) with a value entered by parameter as right operand. _mulassign
: Implements multiplication assignment operator (aka *= ) with a value entered by parameter as right operand. _neg
: Implements negate pre operator (aka -a
). _not
: Implements not pre operator (aka !a
) _orassign
: Implements bitwise OR assignment operator (aka |=
) with a value entered by parameter as right operand. _postdec
: Implements post decrement operator (aka a--
) _postinc
: Implements post increment operator (aka a++
). _predec
: Implements pre decrement operator (aka --a
). _preinc
: Implements pre increment operator (aka ++a
). _set
: Implements assignment operator (aka =
) with a value entered by parameter as right operand. _shlassign
: Implements bitwise SHIFT LEFT assignment operator (aka <⇐
) with a value entered by parameter as right operand. _shrassign
: Implements bitwise SHIFT RIGHT assignment operator (aka >>=
) with a value entered by parameter as right operand. _subassign
: Implements substraction assignment operator (aka -=
) with a value entered by parameter as right operand. _tostring
: Returns custom string when string operation operation is invoved. _xorassign
: Implements bitwise XOR assignment operator (aka ^=
) with a value entered by parameter as right operand
Static metamethods
_add
: Implements add operator (aka +
) between first operand and second operand _and
: Implements bitwise AND operator (aka &
) between first operand and second operand _div
: Implements division operator (aka /
) between first operand and second operand _equ
: Implements equal operator (aka ==
) between first operand and second operand _gt
: Implements greater than operator (aka >
) between first operand and second operand _gte
: Implements greater than or equal operator (aka >=
) between first operand and second operand _lt
: Implements less than operator (aka <
) between first operand and second operand _lte
: Implements less than or equal operator (aka <=
) between first operand and second operand _mod
: Implements modulus operator (aka *
) between first operand and second operand _mul
: Implements multiplication operator (aka *
) between first operand and second operand _nequ
: Implements not equal operator (aka !=
) between first operand and second operand _or
: Implements bitwise OR operator (aka |
) between first operand and second operand _shl
: Implements bitwise SHIFT LEFT operator (aka <<
) between first operand and second operand _shr
: Implements bitwise SHIFT RIGHT operator (aka <<
) between first operand and second operand _xor
: Implements bitwise XOR operator (aka ^
) between first operand and second operand
In the following code, it shows an example of the implementation _add
and _tostring
metamethods:
class Number{
constructor(_value=0){
this.__value__=_value;
}
static _add(_op1,_op2){
var op1,op2
if(_op1 instanceof Integer || _op1 instanceof Float){
op1 = _op1;
}else if(_op1 instanceof Number){
op1 = _op1.__value__;
}else{
System::error("Number::_add : right operand not supported");
}
if(_op2 instanceof Integer || _op2 instanceof Float){
op2 = _op2;
}else if(_op2 instanceof Number){
op2 = _op2.__value__;
}else{
System::error("Number::_add : left operand not supported");
}
return new Number(op1+op2);
}
_tostring(){
return this.__value__;
}
};
Console::outln("new Number(10) + new Number(20) => " + (new Number(10) + new Number(20)));
Console::outln("new Number(10) + 20 => " + (new Number(10) + 20));
Console::outln("10 + new Number(20) => " + (10 + new Number(20)));
For more information about how to implement other metamethods and its examples, please refer to the ZetScript documentation.
Properties
A property is a kind of variable that is accessed through a metamethods.
class Test{
property{
...
}
}
ZetScript supports the following implementation of metamethods on properties:
_get
: Returns the value of the property _set
: Implements assignment operator (aka =
) with a value entered by parameter as right operand. _addassign
: Implements addition assignment operator (aka +=
) with a value entered by parameter as right operand. _subassign
: Implements substraction assignment operator (aka -=
) with a value entered by parameter as right operand. _mulassign
: Implements multiplication assignment operator (aka *=
) with a value entered by parameter as right operand. _divassign
: Implements division assignment operator (aka /=
) with a value entered by parameter as right operand. _modassign
: Implements modulus assignment operator (aka %=
) with a value entered by parameter as right operand. _andassign
: Implements bitwise AND assignment operator (aka &=
) with a value entered by parameter as right operand. _orassign
: Implements bitwise OR assignment operator (aka |=
) with a value entered by parameter as right operand _xorassign
: Implements bitwise XOR assignment operator (aka ^=
) with a value entered by parameter as right operand. _shrassign
: Implements bitwise SHIFT RIGHT assignment operator (aka >>=
) with a value entered by parameter as right operand. _shlassign
: Implements bitwise SHIFT LEFT assignment operator (aka <⇐
) with a value entered by parameter as right operand. _postinc
: Implements post_increment
operator (aka a++
) _postdec
: Implements post_decrement
operator (aka a--
) _preinc
: Implements pre_increment
operator (aka ++a
) _predec
: Implements pre_decrement
operator (aka --a
)
In the following code, it shows an example of the implementation of value
property and implementing metamethods _addassing
and _get
:
class Number{
constructor(_value=0){
this.__value__=_value;
}
value{
_get(){
return this.__value__
}
_addassign(_op1){
if(_op1 instanceof Integer || _op1 instanceof Float){
this.__value__ += _op1;
}else{
System::error("Number::value::_addassign : right operand not supported");
}
}
}
}
var number=new Number(20);
Console::outln("number.value+=20 => {0}",number.value+=20)
For more information about how to implement other metamethods and its examples, please refer to the ZetScript documentation.
Performance
I had checked ZetScript performance with other script languages. Because Lua is the fastest and one of the most scripting language used, I chose this to compare Fibonacci calculation time with N=34
.
A Fibonacci script implemented in ZetScript is:
function fibR(n)
{
if (n < 2) {
return n;
}
return fibR(n-2)+fibR(n-1);
}
Console::outln("fib: " + (fibR(34)) );
and the equivalent in Lua is:
function fibR(n)
if (n < 2) then return n end
return (fibR(n-2) + fibR(n-1))
end
print("fib: " .. fibR(34))
And I have compared them through the time
command already available in linux and the tests were made on a computer with i5-2450 CPU with 2.50GHz and 8GB of RAM.
The result was the following:
- Lua: 1.142s
- ZetScript: 1.250s
So I can say that Lua is 1.250/1.142=1.09 times faster than ZetScript in this test.
There's also a current list of other benchmarks it has been done vs other script languages in the following link:
Conclusions
I have presented a new programming language, but it is not quite new because it is close to JavaScript so many people can find it comfortable using it. Furthermore, Zetscript API exposes C++ code in script side in a straighforward way so is quite productive in general.
Maybe, it will have success or not. I did this project for my requirements but I made it available for free in case other people can find it useful for their projects.
Changes since 2.0.3
- Massive update that rewrites a internal architecture, organize and clean code
- VM speed up ~x2.5
- VM save memory stack ~x2
- Optimize size/compile times ~x2
- Implements multiple Return/Assignments
- Implements arguments with options: default, variable arguments and by reference
- Implementation of properties with custom metamethods
- Implements keywords
const
- Implements operators
in
,typeof
- Implements builtin iterators for
String
, Array
and Objects
- Implements builtin modules:
System
, Math
, Json
and DateTime
- The use of
function
keyword on class function members are not mandatory switch
-case
eval
expressions - Implements builtin concatenation with Array and Object types through operator '
+
' - Full documentation and working examples
Changes since 1.3.0
- Implements an interactive console
- Added add/remove attributes on
struct
variable - Optimized
eval
process - Improve virtual machine speed ~x2
- Minor bug fixes
Changes since 1.2.0
eval
process can be split in parse/compile and execute (see section 2.4 from ZetScript documentation) - As virtual classes change its memory map at run-time, function and variables cannot ensure the same pointer as base class so it has been decided to disable heritate all functions/variables from parent classes (only C). Due to that change, now we have to pass class type on both
register_C_FunctionMember
and register_C_VariableMember
. - ZetScript 1.1.3 supported automatic register of parent functions/variables but, due to the problem of virtual functions, it cannot do since 1.2. Derived classes have to re-register parent functions/variables in order to use in script side.
- ZetScript 1.1.3 allowed to pass
float
type as arguments but this only works 100% for x32 builds. So to avoid confusion, I decided to constraint pass float
types as pointer (i.e., float *
).
History
- 2024-01-01 ZetScript 2.0.3: Lot of rework (see HISTORY file for more information)
- 2018-04-20 ZetScript 1.3.0: An interactive console (zs.exe) has been implemented, it has improved virtual machine speed and a minor bug fixes. (see HISTORY for more information). Added performance test comparison with Lua script language in this article.
- 2018-02-21 ZetScript 1.2.0: It has some features and a major bug fix so, as far I could test, is stable version. Even though I prefer version 1.1.3, I don't recommend using it because it doesn't work on virtual classes (it has segmentation fault as a hell) and also it doesn't work passing
float
for functions with 2 or more arguments on x64 builds. - 2017-12-01 ZetScript 1.1.3: First release