zengl v1.3.0 , 该版本添加了11个二进制位操作相关的运算符添加了zenglApi_RunStr接口,可以将一段字符串直接当作脚本来解析执行...

    v1.3.0版本的下载地址如下:

    github项目地址为:https://github.com/zenglong/zengl_language/  选择右侧的Download Zip或Clone in Desktop

    百度盘共享链接地址:http://pan.baidu.com/s/1lEeUz 这是一个zip压缩包,解压后可以看到linux ,windows和mac目录,分别为三个操作系统环境下的源码和编译器配置,windows目录中既包含用于vs2008的sln解决方案,还包含用于vc6的dsw工作空间。

    在linux中进行make之前,如果不是root用户,就请用 "su" 之类的命令提权,因为make生成libzengl.so后,会copy这个.so文件到/usr/lib目录中(该目录需要root权限才能进行写入操 作),这样可执行文件zengl和encrypt才能链接和使用libzengl.so的动态链接库,详情可以查看makefile,只有 libzengl.so是zengl语言的核心文件,main.c和encrypt.c生成的zengl和encrypt可执行文件都只是 libzengl.so 的测试C程序 (ubuntu下也可以直接使用sudo make)。

    mac目录中只包含makefile文件和测试用的zengl脚本文件,所以在mac下直接make即可,需要清理中间文件时,可以使用make clean来清理。

    sourceforge.net上的项目地址为:https://sourceforge.net/projects/zengl/files/ (里面有所有历史版本的压缩包,包括v1.3.0的zip包)。

    下面看下v1.3.0所做的修改(这些信息可以在readme或linux的usage.txt中查看到,也可以直接在上面的github链接中查看到):

    zengl v1.3.0 , 该版本添加了"&"(按位与),"&="(按位与赋值),"|"(按位或),"|="(按位或赋值),"^"(按位异或),"^="(按位异或赋值),"<<"(左移),"<<="(左移赋值),">>"(右移),">>="(右移赋值),"~"(按位取反)一共11个二进制位操作相关的运算符

    添加了zenglApi_RunStr接口,可以将一段字符串直接当作脚本来解析执行。

    添加了zenglApi_ReUse接口,如果某虚拟机之前编译过,那么可以通过此接口设置重利用标志,接着就可以在zenglApi_Run或zenglApi_Call或zenglApi_RunStr中跳过编译,直接执行代码,该接口可以看成一种缓存方式。

    添加zenglApi_SetFunArgEx接口,并使用该接口来实现zenglApiBMF_unset内建模块函数,该模块函数可以将变量重置到未初始化状态,最主要的是可以将引用类型的变量或数组成员或类成员重置为普通变量。引用类型也做了调整,引用可以是一级,二级,三级甚至是更多级的引用,这样才能对引用进行重置。

    添加zenglApi_GetModFunName接口,该接口可以在模块函数定义中获取用户定义的模块函数名

    修复一些BUG,例如当字符串脚本或文件型脚本内容为空时,可能会出现的内存访问异常的BUG,以及像obj.key[i % obj.keylen]这样的表达式,类成员数组元素中再访问类成员时可能会出现的递归错误等

    作者:zenglong
    时间:2013年12月16日
    官网:www.zengl.com

    该版本一共添加了11个位运算操作符,下面以"|"(按位或运算符)为例来看下这些运算符是如何添加进来的。

    zengl分为两个部分,一个编译器部分,一个解释器部分,这两个一起构成了虚拟机的主体部分。对于编译器而言,它主要由词法扫描器,语法分析器,符号信息扫描器,汇编指令生成器组成,所以最开始的部分就是词法扫描器部分,该部分会给扫描出来的变量,数字,加减乘除运算符等设置对应的token信息,然后将这些token信息交给语法分析器去构建AST抽象语法树。

    所以第一步就是给按位或运算符添加一个token信息,在zengl_global.h中有一个ZENGL_TOKENTYPE的枚举类型:

/*token节点类型定义*/
typedef enum _ZENGL_TOKENTYPE{

	ZL_TK_START_NONE,	//初始值,不对应任何token
	ZL_TK_ID,		//变量之类的标识符token
	ZL_TK_RESERVE,		//关键字token
	ZL_TK_NUM,		//如123之类的数字的token
	ZL_TK_FLOAT,		//如3.14159之类的浮点数
	........................//省略N行
	ZL_TK_BIT_OR,           //"|"按位或双目运算符token
	........................//省略N行

}ZENGL_TOKENTYPE;
/*token节点类型定义结束*/

    上面类型中,ZL_TK_BIT_OR就是按位或运算符的token枚举值,zengl_main.c里的zengl_getToken函数在扫描脚本中的字符时,就会对扫描到的按位或字符设置该token信息:

/*获取token信息*/
ZENGL_TOKENTYPE zengl_getToken(ZL_VOID * VM_ARG)
{
	........................//省略N行
	case ZL_ST_INOR:
		state = ZL_ST_DOWN;
		if(ch == '|') //两个|在一起即'||'表示token是逻辑或运算符
		{
			compile->makeTokenStr(VM_ARG,ch);
			token = ZL_TK_OR;
		}
		else if(ch == '=')
		{
			compile->makeTokenStr(VM_ARG,ch);
			token = ZL_TK_BIT_OR_ASSIGN; //|=运算符
		}
		else //只有一个"|"则表示按位或运算符
		{
			token = ZL_TK_BIT_OR;
			compile->ungetchar(VM_ARG);
		}
		break;
	........................//省略N行
}

    上面代码中最后一个else表明只有一个"|"字符时,就会将token设为ZL_TK_BIT_OR ,有了token信息后,接下来就需要将该token通过zengl_parser.c里的zengl_ASTAddNode函数加入到AST抽象语法树的节点数组中:

/**
    将token加入AST抽象语法树
*/
ZL_VOID zengl_ASTAddNode(ZL_VOID * VM_ARG,ZENGL_TOKENTYPE token)
{
    ........................//省略N行
    case ZL_TK_BIT_OR:
        compile->AST_nodes.nodes[compile->AST_nodes.count].tokcategory = ZL_TKCG_OP_BITS;
        compile->AST_nodes.nodes[compile->AST_nodes.count].tok_op_level = ZL_OP_LEVEL_BITS;
        break;
    ........................//省略N行
}

    上面代码中,为按位或运算符设置了相应的token分类信息,以及token优先级信息,按位或和按位与等双目位运算符都属于 ZL_TKCG_OP_BITS 分类,该分类也是zengl_global.h中定义的一个枚举值,有了token分类信息,zengl内部就可以将多个token当作一个分类来进行统一处理。ZL_OP_LEVEL_BITS 是双目位运算符的优先级(也是一个枚举值),组建语法树时会根据优先级的高低来进行组建,这个双目位运算符的优先级比大于等于之类的比较运算符的优先级高,比加减运算符的优先级低,所以当按位或运算符遇到大于等于之类的比较运算符时,会先进行位运算,再进行比较运算。

    将token加入语法树节点数组后,就需要对这些节点按照优先级等信息进行相互关联,从而构建出完整的AST抽象语法树出来。按位或等表达式运算符的语法树组建过程都是由zengl_parser.c里的zengl_express函数来完成的:

/**
    第三个版本的语法分析函数。这是编译引擎里最核心的部分,在上一个版本基础上
    算法做了调整,采用纯状态机加优先级堆栈的方式,比第二个版本的可读性强很多
    ,方便维护和扩展。(从v1.2.0开始就一直是这个版本的语法分析函数)
*/
ZL_INT zengl_express(ZL_VOID * VM_ARG)
{
    ........................//省略N行
    switch(compile->exp_struct.state)
    {
    case ZL_ST_START:
    ........................//省略N行
        case ZL_TK_BIT_OR:
            compile->exp_struct.state = ZL_ST_PARSER_INBIT_OR;
            break;
    ........................//省略N行
    case ZL_ST_PARSER_INBIT_OR:
    case ZL_ST_PARSER_INBIT_AND:
    case ZL_ST_PARSER_INEQUAL:
    case ZL_ST_PARSER_INGREAT_EQ:
    ........................//省略N个case
        compile->detectCurnodeSyntax(VM_ARG);
        compile->OpLevelForTwo(VM_ARG);
        break;
}

    在上面zengl_express函数中,如果在 ZL_ST_START 初始状态下遇到了 ZL_TK_BIT_OR 按位或的token节点,就会进入 ZL_ST_PARSER_INBIT_OR 状态,该状态会和大于等于,加减乘除等这些双目运算符的状态一起通过 OpLevelForTwo 函数来组建双目运算符的语法树(OpLevelForTwo 就是 zengl_OpLevelForTwo 函数的函数指针)。

    在由OpLevelForTwo函数组建语法树之前,会先由 zengl_detectCurnodeSyntax 函数进行一些语法检测:

/*根据当前curnode节点和curnode+1节点来初步判断是否有语法错误*/
ZL_VOID zengl_detectCurnodeSyntax(ZL_VOID * VM_ARG)
{
    ........................//省略N行
    switch(compile->exp_struct.state)
    {
    case ZL_ST_INNUM:  //curnode当前节点为数字,浮点数或字符串时
    case ZL_ST_INFLOAT:
    case ZL_ST_INSTR:
        if(ZENGL_AST_ISTOK_VALIDX(nextNodeNum) && 
            ........................
	    nextNodeTKCG == ZL_TKCG_OP_BITS ||
	    ........................
	    )) //数字,浮点数,字符串后面必须是除了赋值.......
            return;
        else if(nextNodeNum != -1)
	    compile->parser_errorExit(...............);
        else
            compile->parser_errorExit(...............);
        break;
    case ZL_ST_INID:
    ........................//省略N行
}

    在上面的检测中,例如在当前处理的节点为数字,浮点数,字符串时,如果该节点后面是 ZL_TKCG_OP_BITS 分类下的按位或,按位与等双目运算符时,就return直接返回以表示检测通过,否则就会通过 parser_errorExit 函数来退出虚拟机并显示相关的语法错误信息,所以在新增了运算符时,都需要在该函数中加入新运算符的检测代码,否则就会产生语法错误。

    下面的 zengl_OpLevelForTwo 函数会根据token节点的优先级来组建双目运算符的语法树:

/*
    使用优先级堆栈处理加减乘除等双目运算符
*/
ZL_VOID zengl_OpLevelForTwo(ZL_VOID * VM_ARG)
{
    ........................//省略N行
    while(ZL_TRUE) //循环进行堆栈优先级的比较
    {
    ........................//省略N行
    }
}

    这个函数的代码差不多已经定型了,在新增双目运算符时,只要之前定义好优先级,它就会自动生成对应的语法树。

    语法树生成完后,就是汇编生成指令代码部分,这部分就是由zengl_assemble.c中的zengl_AsmGenCodes函数来完成的:

/*
    该函数根据AST抽象语法树的节点索引将某节点转为汇编代码。
    参数nodenum为节点在AST语法树动态数组里的节点索引。
*/
ZL_VOID zengl_AsmGenCodes(ZL_VOID * VM_ARG,ZL_INT nodenum)
{
    .............................
    case ZL_ST_ASM_CODE_INBITS: //按位与,或,异或等双目位运算符
    .............................
        case ZL_TK_BIT_OR:
            run->AddInst(VM_ARG, compile->gencode_struct.pc++, nodenum,
                    ZL_R_IT_BIT_OR , ZL_R_DT_NONE , 0,
                    ZL_R_DT_NONE , 0); //对应汇编指令 "BIT_OR" 按位或汇编码 BIT_OR指令会将AX 和 BX寄存器的值进行按位或运算,结果存放在AX中。
            if(nodes[nodenum].toktype == ZL_TK_BIT_OR_ASSIGN)
                goto assign;
            break;
	.............................
}

    当在语法树中遇到按位或运算符节点时,zengl_AsmGenCodes函数就会调用run解释器里的AddInst函数来将 ZL_R_IT_BIT_OR 按位或的汇编指令枚举值加入到解释器中,这样后面run解释器运行时就可以根据该指令枚举值调用对应的函数来完成按位或操作。

    run解释器的运行主要是通过zenglrun_main.c文件里的zenglrun_RunInsts函数来执行的:

/**
    虚拟机解释器执行汇编指令的主程式。
*/
ZL_VOID zenglrun_RunInsts(ZL_VOID * VM_ARG)
{
    .............................
    case ZL_R_IT_BIT_OR:
    .............................
        run->op_bits(VM_ARG); //按位与,或,异或等位运算指令的处理程式
        break;
    .............................
}

    上面代码中,当zenglrun_RunInsts函数遇到 ZL_R_IT_BIT_OR 按位或等位运算指令时就会调用 op_bits 函数来执行具体的位运算:

/*按位与,或,异或等位运算指令的处理程式*/
ZL_VOID zenglrun_op_bits(ZL_VOID * VM_ARG)
{
    .............................
    case ZL_R_IT_BIT_OR:
        ZENGL_RUN_REGVAL(ZL_R_RT_AX).dword = ZENGL_RUN_REGVAL(ZL_R_RT_AX).dword | ZENGL_RUN_REGVAL(ZL_R_RT_BX).dword;
        break;
    .............................
}

    当执行按位或指令时,就会由上面的代码通过C语言的按位或运算符对AX,BX寄存器里的值进行按位或的位运算,结果存储在AX中。

    以上就是位运算符的添加和解析过程,另外在zengl_local.c文件中,每个token和每个汇编指令都对应有一个用于调试的字符串常量信息,而且是一一对应的关系,所以在添加运算符token枚举值和汇编指令枚举值时,一定要在该文件中加入对应的字符串常量信息,否则调试日志信息等就可能会混乱。

    在该版本的测试脚本test.zl中就使用新增的位运算符来实现了RC4的加密和解密运算:

use builtin;
class RC4
    state;
    key;
    keylen;
    fun init(obj)
        RC4 obj;
        obj.key = array(97,98,99,100); //对应 a , b , c , d
        obj.keylen = 4;
        for(i=0;i<256;i++) //将盒子里的元素用0到255初始化
            obj.state[i] = i;
            /*if(i < 5)
                print 'state_init['+i+']:' + obj.state[i];
            endif*/
        endfor
        for(i=0,j = 0; i < 256; ++i) //将盒子里的元素顺序打乱
            j = (j + obj.state[i] + obj.key[i % obj.keylen]) & 0xff; 
            t = obj.state[i]; 
            obj.state[i] = obj.state[j]; 
            obj.state[j] = t; 
            /*if(i < 5)
                print 'state_initkey['+i+']:' + obj.state[i];
            endif*/
        endfor
    endfun

    fun encrypt(obj,str,str_len)
        RC4 obj;
        i = j = 0;
        for(cur = 0;cur < str_len;cur++)
            i = (i + 1) & 0xff;
            j = (j + obj.state[i]) & 0xff;
            t = obj.state[i];
            obj.state[i] = obj.state[j]; 
            obj.state[j] = t;
            str[cur] = obj.state[(obj.state[i] + obj.state[j]) & 0xff] ^ str[cur];
            if(cur < 5)
                print 'encrypt['+cur+']:' + obj.state[(obj.state[i] + obj.state[j]) & 0xff];
            endif
        endfor
    endfun
endclass

RC4 rc4;
rc4 = array();
RC4.init(rc4);
RC4.encrypt(rc4,str = array(104,105,106,107,108),5); //对应 h , i , j , k , l
bltPrintArray(str);
RC4.init(rc4);
RC4.encrypt(rc4,str ,5);
bltPrintArray(str);

.............................

    上面的测试脚本中使用a,b,c,d四个字符的十进制ASCII码即array(97,98,99,100)作为RC4的初始密钥,然后对h,i,j,k,l的十进制ASCII码即array(104,105,106,107,108)进行加密和解密运算,通过bltPrintArray内建模块函数将运算后的数组元素打印显示出来。

    该版本还添加了zenglApi_RunStr接口,可以将一段字符串直接当作脚本来解析执行,例如在main.c的入口函数部分:

/**
    用户程序执行入口。
*/
int main(int argc,char * argv[])
{
    .............................
    char * run_str = 
        "//this is a test for zenglApi_RunStr \n"
        "inc 'test2.zl';\n"
        "a = 135; \n"
        "b=2;\n"
        "print 'a-b is '+(a-b); \n"
        "bltLoadScript('test3.zl'); \n"; //行尾设置\n换行符这样查找语法错误时,方便定位行号信息
    .............................
    printf("\n[next test zenglApi_RunStr]: \n");
    if(zenglApi_RunStr(VM,run_str,run_str_len,"runstr") == -1) //编译执行字符串脚本
        main_exit(VM,"错误:编译runstr失败:%s\n",zenglApi_GetErrorString(VM));
    .............................
}

    上面代码中,run_str是一个字符串指针,指向的字符串里面使用zengl语法写了一小段脚本程序,接着就可以使用zenglApi_RunStr接口来编译运行这段字符串脚本。这样脚本就不一定非要写在文件里面了,可以从外界接收一段用户输入的字符串信息,然后将这段字符串信息作为脚本来解析执行。

    main.c测试程序里还用到了该版本中新增的zenglApi_ReUse接口:

int main(int argc,char * argv[])
{
    .............................

    printf("\n[next test zenglApi_Call and zenglApi_ReUse]: \n");

    zenglApi_Reset(VM);

    zenglApi_Push(VM,ZL_EXP_FAT_INT,0,1415,0);

    zenglApi_Push(VM,ZL_EXP_FAT_INT,0,3,0);

    if(zenglApi_Call(VM,"test2.zl","test","clsTest") == -1) //编译执行zengl脚本里的类函数
            main_exit(VM,"错误:编译<test fun call>失败:%s\n",zenglApi_GetErrorString(VM));

    zenglApi_ReUse(VM,0); //不清理虚拟内存的ReUse
    
    zenglApi_Push(VM,ZL_EXP_FAT_INT,0,14,0);

    zenglApi_Push(VM,ZL_EXP_FAT_FLOAT,0,0,3.14);

    if(zenglApi_Call(VM,"test2.zl","test","clsTest") == -1) //编译执行zengl脚本里的类函数
        main_exit(VM,"错误:编译<test fun call>失败:%s\n",zenglApi_GetErrorString(VM));

    zenglApi_ReUse(VM,1); //清理虚拟内存的ReUse

    if(zenglApi_Call(VM,"test2.zl","test","clsTest") == -1) //编译执行zengl脚本里的类函数
        main_exit(VM,"错误:编译<test fun call>失败:%s\n",zenglApi_GetErrorString(VM));

    .............................
}

    上面代码中在zenglApi_Reset重置了VM虚拟机后,通过zenglApi_Call接口编译执行了一次test2.zl脚本里的函数后,VM虚拟机中就存放了test2.zl的编译好的资源,比如该脚本的符号表信息,汇编指令信息等。接着就可以通过zenglApi_ReUse接口来设置该虚拟机内部的重利用标志,设置好后,接下来的zenglApi_Call或zenglApi_Run等在执行时就会跳过编译过程,直接使用之前编译好的汇编指令来执行脚本。

    zenglApi_ReUse的第二个参数表示,是否要清理之前脚本执行过程中全局变量所遗留的值,为0表示不清理全局变量等虚拟内存里遗留的值,这样第二次执行时还可以继续使用之前的全局变量里的值,如果该接口第二个参数不为0则表示要将所有全局变量所在的虚拟内存重置为未初始化状态。

    下面是测试情况:


图1

    上图中第一次运行时test2.zl脚本里的test2全局变量一开始是默认值0,在zenglApi_Call接口执行完test2.zl里的类函数后,test2全局变量就被修改为1418,第二次在zenglApi_ReUse设置重利用后,由于没设置清理虚拟内存,所以zenglApi_Call调用test2.zl里相同的类函数时,test2全局变量的值就为上一次遗留下来的1418,然后在类函数执行时被修改为17.14,第三次zenglApi_ReUse设置了清理标志,所以zenglApi_Call调用时,test2全局变量就被重置为未初始化状态了,在打印显示时,未初始化状态等效于整数零。

    该版本经过调整后,引用类型的变量就可以是一级,二级,三级甚至是更多级的引用,例如 a = &b; c=&a; 这两条代码中,假设b是非引用类型的普通变量,则a就是变量b的一级引用,c就是引用a的引用即二级引用。如果是v1.3.0之前的版本,那么这两条代码执行后,a和c都将是变量b的一级引用,因为在设置引用时,内部做了递归处理,但是这样一来就没办法对引用类型的变量进行操作了,例如unset(&a),这个函数第一个参数因为引用递归的原因将会是变量b的一级引用,那么该函数内部就只能操作变量b,而永远无法操作变量a,所以该版本做了调整,取消了引用在赋值时可能出现的递归情况,这样就可以对引用本身进行操作,比如将引用类型的变量重置为未初始化状态,重置后就可以像普通变量那样参与运算了 (引用类型的变量是无法参与加减乘除之类的运算的,所以这类变量要参与运算,就必须先重置为未初始化状态)。

    为了能对引用进行操作,该版本新增了zenglApi_SetFunArgEx接口,可以对一级,二级甚至更多级的引用进行设置,然后使用该接口实现了zenglApiBMF_unset内建模块函数,该模块函数可以将参数引用的变量重置为未初始化状态,在test.zl测试脚本中就用到了这个模块函数:

.............................

e = &a;
print 'e = &a so now e is address of a';
print 'e |= 0x9 is ' + e|=0x9;
print 'now e is ' + e + ' and a is ' + a + ' they are same';
unset(&e);
e = 10;
print 'unset(&e) then e=10 now e is ' + e + ' a is ' + a + ' so e is not address of a use unset!';

.............................

    上面的脚本代码中,unset就是测试程序中对zenglApiBMF_unset自定义的脚本函数名,代码里先将e设置为变量a的引用,这样对e的操作其实就是对a的操作,接着使用unset模块函数将引用类型的变量e重置为未初始化状态,这样e就不再是其他变量的引用了,变量e就可以参与后面的运算了。

    unset模块函数可以接受多个参数,模块函数内部会循环将每个参数所引用的变量依次重置为未初始化状态。

    另外,该版本zenglApi_BltModFuns.c里定义的内建模块函数内部都调用了一个新增的zenglApi_GetModFunName接口,该接口可以获取到用户程序端设置的模块函数名:

/*unset模块函数,将所有参数所引用的变量或数组元素或类成员等重置为未初始化状态
  未初始化状态在很多场合可以产生和整数0一样的效果,该模块函数最主要的是可以用
  来重置引用类型的变量*/
ZL_EXP_VOID zenglApiBMF_unset(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
    .............................
    if(argcount <= 0)
    {
        zenglApi_GetModFunName(VM_ARG,&modfun_name);
        zenglApi_Exit(VM_ARG,"%s函数的参数个数必须大于0",modfun_name);
    }
    .............................
}

    上面的代码中,当模块函数出错退出时,通过zenglApi_GetModFunName接口就可以获取和显示出用户自定义的模块函数名。

    最后,该版本还修复了一些BUG,例如当字符串脚本或文件型脚本内容为空时,可能会出现的内存访问异常的BUG,以及像obj.key[i % obj.keylen]这样的表达式,类成员数组元素中再访问类成员时可能会出现的递归错误,还有浮点数和整数在某些情况下可能会出现的比较结果不对的BUG,限于篇幅就不一一说明了,可以在github中查看该版本对上一个版本所做的改动。

    有关v1.3.0版本的介绍就到这里。

    OK,到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: v1.3.1 Android编译执行zengl脚本

上一篇: zengl v1.2.5 RC4加密,64位系统及Mac系统编译,Api调用位置规范,内建模块函数

相关文章

zengl编程语言v0.0.7

zengl编程语言v0.0.16数组,21点扑克小游戏

zengl v1.2.4 脚本加密,单目负号,VC6编译,新增API,BUG修复

zengl编程语言v0.0.9流程控制语句的实现

zengl编程语言v0.0.5构建符号表汇编代码和虚拟机

zengl编程语言v0.0.20 inc加载脚本