Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

ZetScript Programming Language

4.98/5 (40 votes)
28 Feb 2024MIT10 min read 51.5K   1K  
A simple script engine for C++
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:

C++
#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
  1. Download and decompress ZetScript project in some directory.
  2. 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` )
  3. Go to the ZetScript project directory which was decompressed and create building information through cmake with the following command:
    BAT
    cmake -Bbuild

    Optionally, you can pass the following options in cmake parameters:

    • -DCMAKE_BUILD_TYPE={Release|Debug}: Configures ZetScript project for Release or Debug (by default is Release).
    • -DTESTS:BOOL={true|false}: Configures ZetScript to build or not tests (by default is FALSE).
    • -DSAMPLES:BOOL={true|false}: Configures ZetScript to build or not samples (by default is FALSE).
    • -DBUILD_SHARED_LIBS:BOOL={true|false}: Configures ZetScript to build as SHARED (by default is FALSE, i.e., it builds as STATIC).
  4. In Linux, MacOS and MingW environments compile the project with the following command:
    Shell
    make -C build

    For Windows using Visual Studio, compile the project with the following command:

    BAT
    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.

ZetScript
var i=10;         //integer
var f=0.5;        // float
var s="a string"; // string
var b=true;       // boolean

var array=[1,0.5, "a string", true]; // array

var object={      // 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 vars are destroyed when it exits from block, unless it is not referenced by other variable.

ZetScript
var i= 0; // global var (never is destroyed when is declared)

{ // starting block --> declaring local variables starts here. 
  // You can access also to global var.

	var j= 2; // local var 
	// ...

} // ending block --> j is destroyed

Conditionals

ZetScript supports if-else and switch conditionals:

ZetScript
// if-else conditional
var number=5;
if(number < 10){
    Console::outln("number less than 10");
}else{
    Console::outln("number greater equal than 10");
}

// switch conditional
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:

ZetScript
var i=0;
// while loop
while(i < 10){
    Console::outln("i:"+i);
    i++;
}

// do-while loop
do{
    Console::outln("i:"+i);
    i++;
}while(i < 20);

// for loop
for(var j=0; j < 10; j++){
    Console::outln("j:"+i);
}

// also for-in to iterate within array/object elements
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.

ZetScript
var n_entity=1;

class Entity{
    // Member variable initialization (this.__id__)
    var __id__=0;
    
    // Member const variable (Acces by Entity::MAX_ENTITIES)
    const MAX_ENTITIES=10;
    
    // Static member function
    static isEntityDead(_entity){
        return _entity.health==0;
    }

    // Member function metamethod _equ to make Entity comparable by '=='
    static _nequ(_e1,_e2){
        return _e1.id!=_e2.id;
    }
    
    // constructor
    constructor(_name="unknown",_health=0){
        this.name=_name
                this.__id__=n_entity++;
                this.setHealth(_health);
    }

    // Member function Entity::update()
    update(){
        Console::outln("From Entity")
    }
    
    // Member property Entity::id
    id{
        // Member property metamethod _get only for read
        _get(){
            return this.__id__;
        }
    }
}

// Implement Entity::setHealth
function Entity::setHealth(_health){
    this.health = _health;
}

class Player extends Entity{
    constructor(){
        super("Player",10);
    }

    // Override Entity::update
    update(){
        // Calls Entity::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();

// invokes operator !=, Entity::_equ member function
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.

C++
#include "zetscript.h"

// C function to register
void sayHelloWorld(zetscript::ScriptEngine *_script_engine){
    printf("Hello world\n");
}

int main(){
    zetscript::ScriptEngine script_engine;

    // Registers 'sayHelloWorld' function
    script_engine.registerFunction("sayHelloWorld",sayHelloWorld);

    // Compiles and run a script where it calls 'sayHelloWorld' function
    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.

C++
#include "zetscript.h"

int main(){

    zetscript::ScriptEngine script_engine;

    // Compiles script function 'sayHello'
    script_engine.compile(
        "function sayHello(){\n"
        "    Console::outln(\"call from 'sayHello'!\")\n"
        "}\n"
    );

    // binds script function 'sayHello'
    auto  say_hello=script_engine.bindScriptFunction<void ()>("sayHello");

    // it calls 'say_hello' script function from C++
    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.

C++
#include "zetscript.h"

int n_entity=1;

// Class to register
class EntityNative{
public:
    const static int MAX_ENTITIES=10;

    int __id__=0;
    int health=0;
    std::string name="entity";
};

//-----------------------
// REGISTER FUNCTIONS

// Register function that returns a new instance of native 'EntityNative'
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;
}

// Register function that deletes native instance of 'EntityNative'
void EntityNative_delete(
    zetscript::ScriptEngine *_script_engine
    ,EntityNative *_this
){
    delete _this;
}

// Register function that implements EntityNative::constructor
void EntityNative_constructor(
    zetscript::ScriptEngine *_script_engine
    ,EntityNative *_entity
    ,zetscript::String *_name
    ,zetscript::zs_int _health
){
    _entity->name=_name->toConstChar();
    _entity->health=_health;
}

// Register function that implements const member variable EntityNative::MAX_ENTITIES
zetscript::zs_int EntityNative_MAX_ENTITIES(
    zetscript::ScriptEngine *_script_engine
){
    return EntityNative::MAX_ENTITIES;
}

// Register function that implements static member function EntityNative::isEntityDead
bool EntityNative_isEntityDead(
    zetscript::ScriptEngine *_script_engine
    ,EntityNative *_entity
){
    return _entity->health==0;
}

// Register function that implements static member function 
// metamethod EntityNative::_equ (aka ==)
bool EntityNative_nequ(
    zetscript::ScriptEngine *_script_engine
    ,EntityNative *_e1
    ,EntityNative *_e2
){
    return _e1->__id__!=_e2->__id__;
}

// Register function that implements member function metamethod EntityNative::update
void EntityNative_update(
    zetscript::ScriptEngine *_script_engine
    ,EntityNative *_this
){
    printf("Update from EntityNative\n");
}

// Register function that implements member property id metamethod getter
zetscript::zs_int EntityNative_id_get(
    zetscript::ScriptEngine *_script_engine
    ,EntityNative *_this
){
    return _this->__id__;
}

// Register function that implements member property name metamethod getter
zetscript::String EntityNative_name_get(
    zetscript::ScriptEngine *_script_engine
    ,EntityNative *_this
){
    return _this->name.c_str();
}

// REGISTER FUNCTIONS
//-----------------------

int main(){

    zetscript::ScriptEngine script_engine;

    // Register type EntityNative
    script_engine.registerType<EntityNative>
           ("EntityNative",EntityNative_new,EntityNative_delete);

    // Register EntityNative constructor
    script_engine.registerConstructor<EntityNative>(EntityNative_constructor);

    // Register constant member variable EntityNative::MAX_ENTITIES
    script_engine.registerConstMemberProperty<EntityNative>
                  ("MAX_ENTITIES",EntityNative_MAX_ENTITIES);

    // Register static member function EntityNative::isEntityDead
    script_engine.registerStaticMemberFunction<EntityNative>
                  ("isEntityDead",EntityNative_isEntityDead);

    // Register member function metamethod EntityNative::_equ (aka !=)
    script_engine.registerStaticMemberFunction<EntityNative>("_nequ",EntityNative_nequ);

    // Register member function EntityNative::update
    script_engine.registerMemberFunction<EntityNative>("update",EntityNative_update);

    // Register member property id getter
    script_engine.registerMemberPropertyMetamethod<EntityNative>
                  ("id","_get",EntityNative_id_get);

    // Register member property name getter
    script_engine.registerMemberPropertyMetamethod<EntityNative>
                  ("name","_get",EntityNative_name_get);

    // Compiles and runs script
    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.

C++
// Compiles and runs script
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:

ZetScript
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.

ZetScript
class Test{
  // property
  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:

ZetScript
class Number{
    constructor(_value=0){    
        this.__value__=_value;
    }
    
    // property '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:

ZetScript
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:

Lua
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

License

This article, along with any associated source code and files, is licensed under The MIT License