65.9K
CodeProject 正在变化。 阅读更多。
Home

ZetScript 编程语言

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (38投票s)

2017 年 11 月 28 日

MIT

10分钟阅读

viewsIcon

52388

downloadIcon

1161

一个简单的 C++ 脚本引擎

源代码已在 github 上提供。

其他链接

引言

ZetScript 是一种编程语言,其 API 允许将 C++ 代码绑定到脚本端。ZetScript 包含以下特性

  • 虚拟机
  • 接近 JavaScript 的脚本语言
  • 动态垃圾回收器
  • 一种简单的方式将 C++ 代码暴露给 ZetScript 并将 ZetScript 代码绑定到 C++
  • 通过元方法在类型和成员属性上实现运算符

下面的代码中展示了一个 helloworld 示例

#include "zetscript.h"

int main(){

	zetscript::ScriptEngine script_engine;

	script_engine.compileAndRun("Console::outln(\"Hello World from ZetScript!\")");

	return 0;
}
列表 1.1

列表 1.1 中呈现的代码实例化脚本引擎,并编译运行一行代码,使用内置的 Console::outln 打印 Hello World from ZetScript

构建 ZetScript

ZetScript 的编译和执行已在以下平台进行测试

  • Linux (通过 gcc 工具链 >= 4.8)
  • macOS (通过 clang 工具链)
  • Windows (通过 Mingw >= 4.8 或 VisualStudio 2015,2017/2019)
  1. 在某个目录中下载并解压 ZetScript 项目。
  2. 在 Linux 或 MacOs 中打开终端,或者在 Windows 中打开命令提示符终端 (Windows 使用 Visual Studio 时,请使用 `Developer Command Prompt` 打开终端)
  3. 进入解压后的 ZetScript 项目目录,并使用以下命令通过 cmake 创建构建信息
    cmake -Bbuild

    可选地,您可以在 cmake 参数中传递以下选项

    • -DCMAKE_BUILD_TYPE={Release|Debug}:为 Release 或 Debug 配置 ZetScript 项目 (默认为 Release)。
    • -DTESTS:BOOL={true|false}:配置 ZetScript 构建或不构建测试 (默认为 FALSE)。
    • -DSAMPLES:BOOL={true|false}:配置 ZetScript 构建或不构建示例 (默认为 FALSE)。
    • -DBUILD_SHARED_LIBS:BOOL={true|false}:配置 ZetScript 构建为 SHARED (默认为 FALSE,即构建为 STATIC)。
  4. 在 Linux、MacOS 和 MingW 环境中,使用以下命令编译项目
    make -C build

    对于使用 Visual Studio 的 Windows 用户,请使用以下命令编译项目

    msbuild build\zetscript.sln /property:Configuration=Release

构建完成后,ZetScript 库和命令行工具将放置在 bin 目录中。

又一个脚本语言?

2016 年,我决定开始从事脚本开发,因为我需要提高生产效率。当然,作为一个追求卓越的工程师,我开始尝试了世界上已有的所有脚本引擎。

有些脚本引擎速度更快,但我不喜欢它们的语法。另一些则语法很好,但在运行时很慢,等等。在花了很多时间试用它们并试图决定哪种脚本引擎更适合我的项目后,我发现没有一个完全符合我的需求。

因此,我决定开始开发自己的脚本引擎,除了满足我的需求之外,我还从一些论坛上了解了一个好的脚本引擎应该具备的特性。我将这两者结合起来,就诞生了 ZetScript。

我决定将 ZetScript 公开,因为我喜欢让人们的生活更轻松,万一有人觉得这个工具有用。

语言概述

ZetScript 类型

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]
};

范围

ZetScript 的全局作用域概念很简单,变量在评估作用域的顶部声明。局部变量存在于函数或循环等块中。局部 var 在退出块时销毁,除非它被其他变量引用。

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

条件语句

ZetScript 支持 if-elseswitch 条件语句

// 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;
}

循环

ZetScript 支持 whiledo-whilefor 作为循环迭代器

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);
}

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 概述

从 ZetScript 调用 C++ 代码

要从 ZetScript 调用 C++ 代码,需要定义并注册一个 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;
}

从 C++ 调用 ZetScript

从 C++ 代码调用 ZetScript 需要在编译脚本函数后绑定脚本函数。

#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;
}

将 C++ 类型暴露给 ZetScript

将 C++ 类型暴露给 ZetScript 需要注册 C++ 类型。暴露成员函数或变量需要定义并注册一个 C 函数。下面的示例展示了注册 EntityNative 类的示例。

#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;
}

最后,另一个有趣的特性是 ZetScript 可以从 C 注册的类中扩展脚本类。下面的示例替换了之前的脚本,它表示 Player 继承自 EntityNative

    // 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"
    );

元方法

ZetScript 支持以下元方法实现,以执行对象上的操作。

成员元方法

  • _addassign 实现加法赋值运算符 (即 += ),其中参数作为右操作数
  • _andassign 实现按位与赋值运算符 (即 &= ),其中参数作为右操作数。
  • _divassign 实现除法赋值运算符 (即 /= ),其中参数作为右操作数。
  • _in 实现 in 运算符
  • _modassign:实现模赋值运算符 (即 %= ),其中参数作为右操作数。
  • _mulassign 实现乘法赋值运算符 (即 *= ),其中参数作为右操作数。
  • _neg 实现前置求反运算符 (即 -a )。
  • _not 实现前置逻辑非运算符 (即 !a)
  • _orassign 实现按位或赋值运算符 (即 |=),其中参数作为右操作数。
  • _postdec 实现后置递减运算符 (即 a--)
  • _postinc 实现后置递增运算符 (即 a++)。
  • _predec 实现前置递减运算符 (即 --a)。
  • _preinc 实现前置递增运算符 (即 ++a)。
  • _set:实现赋值运算符 (即 =),其中参数作为右操作数。
  • _shlassign:实现按位左移赋值运算符 (即 <<=),其中参数作为右操作数。
  • _shrassign:实现按位右移赋值运算符 (即 >>=),其中参数作为右操作数。
  • _subassign:实现减法赋值运算符 (即 -=),其中参数作为右操作数。
  • _tostring:在调用字符串操作时返回自定义字符串。
  • _xorassign:实现按位异或赋值运算符 (即 ^=),其中参数作为右操作数。

静态元方法

  • _add:实现第一个操作数和第二个操作数之间的加法运算符 (即 +)
  • _and:实现第一个操作数和第二个操作数之间的按位与运算符 (即 &)
  • _div:实现第一个操作数和第二个操作数之间的除法运算符 (即 /)
  • _equ:实现第一个操作数和第二个操作数之间的等于运算符 (即 ==)
  • _gt:实现第一个操作数和第二个操作数之间的大于运算符 (即 >)
  • _gte:实现第一个操作数和第二个操作数之间的大于等于运算符 (即 >=)
  • _lt:实现第一个操作数和第二个操作数之间的小于运算符 (即 <)
  • _lte:实现第一个操作数和第二个操作数之间的小于等于运算符 (即 <=)
  • _mod:实现第一个操作数和第二个操作数之间的模运算符 (即 %)
  • _mul:实现第一个操作数和第二个操作数之间的乘法运算符 (即 *)
  • _nequ:实现第一个操作数和第二个操作数之间的不等于运算符 (即 !=)
  • _or:实现第一个操作数和第二个操作数之间的按位或运算符 (即 |)
  • _shl:实现第一个操作数和第二个操作数之间的按位左移运算符 (即 <<)
  • _shr:实现第一个操作数和第二个操作数之间的按位右移运算符 (即 >>)
  • _xor:实现第一个操作数和第二个操作数之间的按位异或运算符 (即 ^)

下面的代码展示了 _add_tostring 元方法实现的一个示例。

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)));

有关如何实现其他元方法及其示例的更多信息,请参阅 ZetScript 文档

属性

属性是一种变量,通过元方法进行访问。

class Test{
  // property
  property{
  ...
  }
}

ZetScript 支持以下属性上的元方法实现

  • _get:返回属性的值
  • _set:实现赋值运算符 (即 =),其中参数作为右操作数。
  • _addassign:实现加法赋值运算符 (即 +=),其中参数作为右操作数。
  • _subassign:实现减法赋值运算符 (即 -=),其中参数作为右操作数。
  • _mulassign:实现乘法赋值运算符 (即 *=),其中参数作为右操作数。
  • _divassign:实现除法赋值运算符 (即 /=),其中参数作为右操作数。
  • _modassign:实现模赋值运算符 (即 %=),其中参数作为右操作数。
  • _andassign:实现按位与赋值运算符 (即 &=),其中参数作为右操作数。
  • _orassign:实现按位或赋值运算符 (即 |=),其中参数作为右操作数。
  • _xorassign:实现按位异或赋值运算符 (即 ^=),其中参数作为右操作数。
  • _shrassign:实现按位右移赋值运算符 (即 >>=),其中参数作为右操作数。
  • _shlassign:实现按位左移赋值运算符 (即 <<=),其中参数作为右操作数。
  • _postinc:实现 后置递增 运算符 (即 a++)
  • _postdec:实现 后置递减 运算符 (即 a--)
  • _preinc:实现 前置递增 运算符 (即 ++a)
  • _predec:实现 前置递减 运算符 (即 --a )

下面的代码展示了 value 属性的实现,以及 _addassign_get 元方法的实现示例。

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)

有关如何实现其他元方法及其示例的更多信息,请参阅 ZetScript 文档

性能

我对 ZetScript 的性能与其他脚本语言进行了比较。由于 Lua 是最快且最常用的脚本语言之一,我选择它来比较 Fibonacci 计算时间,其中 N=34

用 ZetScript 实现的 Fibonacci 脚本是

function fibR(n)
{
    if (n < 2) { 
         return n; 
    }

    return fibR(n-2)+fibR(n-1);
}

Console::outln("fib: " + (fibR(34)) );

等效的 Lua 代码是

function fibR(n)

    if (n < 2) then return n end
    return (fibR(n-2) + fibR(n-1))
end

print("fib: " .. fibR(34))

我使用 Linux 中已有的 time 命令进行了比较,测试是在一台拥有 i5-2450 CPU、2.50GHz 主频和 8GB RAM 的计算机上进行的。

结果如下

  • Lua:1.142 秒
  • ZetScript:1.250 秒

因此,我可以得出结论,在此测试中,Lua 比 ZetScript 快 1.250/1.142=1.09 倍

还有一个与其他脚本语言进行的基准测试列表,链接如下

结论

我介绍了一种新的编程语言,但它并不完全新,因为它接近 JavaScript,因此许多人会觉得使用它很方便。此外,Zetscript API 以一种直接的方式暴露 C++ 代码到脚本端,因此总体而言生产力很高。

也许它会成功,也许不会。我开发这个项目是为了满足我的需求,但我免费提供它,以防其他人觉得对他们的项目有用。

2.0.3 版本以来的更改

  • 大规模更新,重写了内部架构,整理并清理了代码
  • 虚拟机速度提升约 2.5 倍
  • 虚拟机节省内存栈约 2 倍
  • 优化大小/编译时间约 2 倍
  • 实现了多返回值/多重赋值
  • 实现了带选项的参数:默认参数、可变参数和按引用传递
  • 实现了带自定义元方法的属性
  • 实现了关键字 const
  • 实现了运算符 intypeof
  • StringArrayObjects 实现内置迭代器
  • 实现了内置模块:SystemMathJsonDateTime
  • 类成员函数中的 function 关键字不再是必需的
  • switch-case 表达式求值
  • 通过运算符 '+' 实现与 Array 和 Object 类型的内置连接
  • 完整的文档和可运行的示例

1.3.0 版本以来的更改

  • 实现了一个交互式控制台
  • struct 变量添加/删除属性
  • 优化了 eval 过程
  • 改进了虚拟机速度约 2 倍
  • 次要 bug 修复

1.2.0 版本以来的更改

  • eval 过程可以拆分为解析/编译和执行 (请参阅 ZetScript 文档的 2.4 节)
  • 由于虚拟类在运行时会改变其内存映射,因此函数和变量无法保证与基类相同的指针,因此决定禁用继承所有函数/变量(仅 C)自父类。由于此更改,我们现在必须在 register_C_FunctionMemberregister_C_VariableMember 中传递类类型。
  • ZetScript 1.1.3 支持自动注册父函数/变量,但由于虚拟函数的问题,自 1.2 版本以来已无法实现。派生类必须重新注册父函数/变量才能在脚本端使用。
  • ZetScript 1.1.3 允许将 float 类型作为参数传递,但这仅在 x32 构建中 100% 有效。为避免混淆,我决定将 float 类型传递限制为指针 (即 float *)。

历史

  • 2024-01-01 ZetScript 2.0.3:大量重构 (更多信息请参阅 HISTORY 文件)
  • 2018-04-20 ZetScript 1.3.0:实现了交互式控制台 (zs.exe),提高了虚拟机速度并修复了一些小错误 (更多信息请参阅 HISTORY)。本文档中添加了与 Lua 脚本语言的性能测试对比。
  • 2018-02-21 ZetScript 1.2.0:包含一些新功能和一个主要的 bug 修复,据我测试,它是一个稳定版本。虽然我更喜欢 1.1.3 版本,但不建议使用它,因为它在虚拟类上无法正常工作 (存在严重的段错误) 且在 x64 构建中,向有两个或更多参数的函数传递 float 也无法正常工作。
  • 2017-12-01 ZetScript 1.1.3:首次发布
© . All rights reserved.