该版本修复zenglApi_ReUse接口可能导致Call接口无法获取正确参数的问题,以及可能的段错误问题,android测试项目使用android studio进行开发

    页面导航:

项目下载地址:

    zengl language v1.8.3的源代码的相关地址:https://github.com/zenglong/zengl_language  当前版本对应的tag标签为:v1.8.3

zengl v1.8.3:

    v1.8.3的版本修复了zenglApi_ReUse接口可能导致Call接口无法获取正确参数的问题,以及修复在linux系统中,ReUse后因重复关闭文件可能报的段错误问题。还完善了语法错误检测,尤其是和print相关的表达式的语法检测。此外,从该版本开始,android目录中的android测试项目使用android studio进行开发。

    之前的像v1.8.2之类的版本没有在vc6,android以及zenglOX中进行测试,导致vc6,android之类的项目代码无法正常编译。而v1.8.3的版本则在windows(vc6和vs2008),linux(主要是CentOS 7和Ubuntu 20.04),Mac OS X(10.13),android(使用Android Studio 4.0.1),zenglOX(3.2.0版本)中都调整了代码和进行了相关测试。

修复zenglApi_ReUse接口:

    该版本修复了zenglApi_ReUse接口的相关问题,该接口的C代码位于zenglApi.c文件中:

/*API接口,重利用虚拟机之前的编译资源,这样就可以直接执行之前已经编译好的指令代码
 参数isClear不为0则执行虚拟内存的清理工作,返回-1表示出错,返回0表示什么都没做,返回1表示正常执行*/
ZL_EXPORT ZL_EXP_INT zenglApi_ReUse(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT isClear)
{
	ZENGL_VM_TYPE * VM = (ZENGL_VM_TYPE *)VM_ARG;
	if(VM->signer != ZL_VM_SIGNER)
		return -1;
	switch(VM->ApiState)
	{
	case ZL_API_ST_AFTER_RUN:
		break;
	default: //其他API状态下直接返回
		return 0;
		break;
	}
	VM->compile.isReUse = ZL_TRUE;
	if(isClear != 0) //isClear不为0则执行虚拟内存的清理工作
	{
		VM->run.FreeAllForReUse(VM_ARG);
	}
	VM->run.reg_list[ZL_R_RT_PC].val.dword = 0; //将PC寄存器重置为0
	VM->run.reg_list[ZL_R_RT_ARGTMP].val.dword = 0; // 将ARGTMP寄存器重置为0
	VM->run.reg_list[ZL_R_RT_LOC].val.dword = 0; // 将LOC寄存器重置为0
	VM->run.reg_list[ZL_R_RT_ARG].val.dword = 0; // 将ARG寄存器重置为0
	VM->run.memblock_freeall_local(VM); // 将之前的栈空间中的内存块释放掉
	VM->run.vstack_list.count = 0;      // 将栈顶位置重置为0
	VM->run.isUserWantStop = ZL_FALSE; //将解释器的停止标志重置为 ZL_FALSE
	VM->isUseApiSetErrThenStop = ZL_FALSE; //重置API中设置的错误停止标志
	VM->ApiState = ZL_API_ST_REUSE;
	return 1;
}

    上面代码中,会在原来的基础上将ARGTMP,LOC,ARG这些和参数以及局部变量相关的寄存器的值都重置为0,还会将之前的栈空间里的内存块给释放掉,并将栈顶位置重置为0。这样,在后面调用zenglApi_Push和zenglApi_ Call接口时,就可以获取到正确的参数,而不会受到之前运行过的脚本函数的参数影响。

段错误处理:

    此外,当前版本修复了可能的段错误问题,修复代码位于zengl_main.c文件中:

/*编译器退出函数,可以输出相关的出错信息*/
ZL_VOID zengl_exit(ZL_VOID * VM_ARG,ZENGL_ERRORNO errorno, ...)
{
	ZENGL_VM_TYPE * VM = (ZENGL_VM_TYPE *)VM_ARG;
	ZENGL_COMPILE_TYPE * compile = &VM->compile;
	...............................................................................
	/**
	 * 编译结束后,如果打开的脚本文件没有被关闭的话(compile->source.file不等于空指针时),就将其关闭掉,否则会发生内存泄露
	 */
	if(compile->source.file != ZL_NULL) {
		ZENGL_SYS_FILE_CLOSE(compile->source.file);
		compile->source.file = ZL_NULL; // 在关闭文件后,将文件指针设置为0,这样在调用ReUse接口后,重利用之前的脚本运行时,就不会因为再次关闭文件而导致段错误
	}
	if(VM->errorno == ZL_NO_ERR_SUCCESS)
		ZENGL_SYS_JMP_LONGJMP_TO(compile->jumpBuffer, 1);
	else
		ZENGL_SYS_JMP_LONGJMP_TO(compile->jumpBuffer, -1);
}

    上面在将compile->source.file文件指针关闭后,还需要将该指针设置为ZL_NULL也就是0,这样在调用了ReUse接口后,重利用之前的脚本运行时,就不会因为再次关闭已经关闭过了的文件,而导致段错误。该段错误在linux中会出现,但是windows中运行时不会出现。

完善语法错误检测:

    当前版本完善了语法错误检测,尤其是和print相关的表达式的语法错误检测,相关代码位于zengl_parser.c文件中:

/**
	生成语句比如print 'hello'之类的语句的AST语法树,statement会调用express(第三个版本的语法分析函数)来生成语句中表达式的AST
*/
ZL_INT zengl_statement(ZL_VOID * VM_ARG)
{
	....................................................................................

	else if(nodes[compile->parser_curnode+1].toktype == ZL_TK_RESERVE)
	{
		compile->parser_addcurnode(VM_ARG);
		switch(nodes[compile->parser_curnode].reserve)
		{
		case ZL_RSV_PRINT:
			p = compile->parser_curnode;
			child_exp_no = compile->express(VM_ARG);
			if(child_exp_no < 0) { // 当child_exp_no小于0时,说明print后面是一个无效的表达式,例如 print ; 这个语句就是无效的语句,此时会返回相应的语法错误
				compile->parser_errorExit(VM_ARG,ZL_ERR_CP_SYNTAX_INVALID_EXP_AFTER_PRINT);
			}
			else
				compile->ASTAddNodeChild(VM_ARG,p,child_exp_no);
			break;
		....................................................................................
		}
	}
	....................................................................................
}

/**
	第三个版本的语法分析函数。这是编译引擎里最核心的部分,在上一个版本基础上算法做了调整,
	采用纯状态机加优先级堆栈的方式,比第二个版本的可读性强很多,方便维护和扩展。
*/
ZL_INT zengl_express(ZL_VOID * VM_ARG)
{
	....................................................................................
	while(compile->exp_struct.state!= ZL_ST_DOWN)
	{
		switch(compile->exp_struct.state)
		{
		case ZL_ST_START:
			compile->parser_addcurnode(VM_ARG);
			switch(nodes[compile->parser_curnode].toktype)
			{
			case ZL_TK_ID:
				compile->exp_struct.state = ZL_ST_INID;
				break;
			case ZL_TK_NUM:
				compile->exp_struct.state = ZL_ST_INNUM;
				break;
			case ZL_TK_FLOAT:
				compile->exp_struct.state = ZL_ST_INFLOAT;
				break;
			....................................................................................
			case ZL_TK_BIT_REVERSE:
				compile->exp_struct.state = ZL_ST_PARSER_INBIT_REVERSE;
				break;
			default: // 此处如果没有default默认处理的话,就会出现解析表达式时,跳过某些token的情况,就无法定位语法错误的具体位置和原因,还可能导致错误的语法解析
				compile->parser_errorExit(VM_ARG,ZL_ERR_CP_SYNTAX_PARSER_EXPRESS_INVALID_TOKEN);
				break;
			}
			break;
		case ZL_ST_INNUM:
		....................................................................................
		}//switch(compile->exp_struct.state)
	}//while(compile->exp_struct.state!= ZL_ST_DOWN)
	return compile->exp_struct.p;
}

    上面在zengl_statement函数中,当child_exp_no小于0的时候,说明print语句后面是一个只包含了分号的空的表达式,也就是print ; 这样的语句,目前编译器在生成print语句的虚拟汇编指令时,无法生成空表达式的虚拟汇编指令,因此,这种写法目前是无效的语法,因为编译器不知道你要打印什么数据。

    之前的版本,在遇到 print ; 这样的语句时,会在生成print的虚拟汇编指令时报错,但是之前版本的错误信息无法定位具体的错误位置和原因,所以,当前版本在child_exp_no小于0时,就直接给出print关键字后面的表达式无效的错误信息,这样可以快速定位到语法错误的具体位置和原因。

    此外,在zengl_express函数中,在对nodes[compile->parser_curnode].toktype进行token的类型判断从而设置相应的状态机时,加入了default的默认处理,这里的默认处理就是直接报语法错误,因为如果此处没有default默认处理的话,像 print endfun 这样的语句,编译器在解析print后面的表达式时,之前的版本会因为endfun是一个关键字token,不在上面的switch...case的判断条件中,它就会跳过endfun这个token,相当于endfun从源代码中被移除掉了一样,从而导致错误的语法解析,并且比较难定位语法错误的具体位置和具体原因。

    而有了default默认处理后,这样的token就不会被跳过去,并且会将引起该错误的token的位置(也就是语法错误的具体位置)和语法错误原因给显示出来。这里,会提示表达式中的token无效,此token不能用于表达式的语法错误信息。

bltFatalErrorCallback:

    为了测试修复后的zenglApi_ReUse接口,和zenglApi_Call接口,在测试代码中加入了bltFatalErrorCallback模块函数,该模块函数在linux/main.c里的相关代码如下:

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

typedef struct{
	ZL_EXP_CHAR * function_name; // 回调函数名
	ZL_EXP_CHAR * class_name;    // 如果回调函数属于某个类,即回调函数是某个类里的方法,则该字段会记录具体的类名
	ZL_EXP_CHAR * error_string;  // 记录具体的运行时错误信息
	int default_cmd_action;      // 是否需要执行默认动作,默认情况下,会将错误信息输出到命令行,如果在脚本的回调函数里,已经将错误信息输出到了命令行的话,可以将该字段设为0,从而不会将错误信息再次输出到命令行
}FatalError_Type; // 脚本发生严重的运行时错误时,需要调用的脚本中的回调函数,脚本可以在该回调函数中获取错误信息和函数栈追踪信息,从而可以在脚本中直接处理错误信息(例如写入日志或输出到终端等)

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

// MainFatalError全局变量用于记录,当发生运行时错误时,脚本回调函数的相关信息,例如脚本回调函数的函数名,所属的类名等
FatalError_Type MainFatalError = {0};

/**
 * 获取函数栈追踪信息,从而可以知道当前执行的代码,是从哪些函数(或者脚本的哪个位置)执行进来的
 */
static void main_get_stack_backtrace(ZL_EXP_VOID * VM_ARG, MAIN_INFO_STRING * debug_info);

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

/**
 * 设置运行时错误回调函数名
 */
static void main_fatal_error_set_function_name(ZL_EXP_CHAR * function_name)
{
	MainFatalError.function_name = main_fatal_error_copy_string(function_name, MainFatalError.function_name);
}

/**
 * 设置运行时错误回调相关的类名,如果回调函数属于某个类中定义的方法的话,就需要通过此函数来设置回调相关的类名
 */
static void main_fatal_error_set_class_name(ZL_EXP_CHAR * class_name)
{
	MainFatalError.class_name = main_fatal_error_copy_string(class_name, MainFatalError.class_name);
}

/**
 * 设置运行时错误发生时,需要传递给回调函数的错误信息
 */
static void main_fatal_error_set_error_string(ZL_EXP_CHAR * error_string)
{
	MainFatalError.error_string = main_fatal_error_copy_string(error_string, MainFatalError.error_string);
}

/**
 * 当脚本发生运行时错误时,如果脚本中设置过运行时错误回调函数的话,就调用该函数来处理运行时错误,
 * 同时会将错误信息和函数栈追踪信息,通过参数传递给回调函数
 */
static int main_fatal_error_callback_exec(ZL_EXP_VOID * VM, ZL_EXP_CHAR * script_file, ZL_EXP_CHAR * fatal_error)
{
	MAIN_INFO_STRING debug_info = {0};
	if(MainFatalError.function_name == NULL) {
		return 0;
	}
	main_get_stack_backtrace(VM, &debug_info);
	zenglApi_ReUse(VM,0);
	zenglApi_Push(VM,ZL_EXP_FAT_STR,fatal_error,0,0);
	zenglApi_Push(VM,ZL_EXP_FAT_STR,debug_info.str,0,0);
	main_free_info_string(VM, &debug_info);
	if(zenglApi_Call(VM, script_file, MainFatalError.function_name, MainFatalError.class_name) == -1) {
		return -1;
	}
	return 0;
}

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

/**
 * bltFatalErrorCallback模块函数,设置当发生运行时错误时,需要调用的脚本函数名,如果是类中定义的方法,还可以设置相关的类名
 * 第一个参数function_name表示需要设置的脚本函数名(必须是字符串类型,且不能为空字符串)
 * 第二个参数class_name是可选参数,表示需要设置的类名(也必须是字符串类型,如果第一个参数function_name是某个类中定义的方法的话,就可以通过此参数来设置类名)
 * 		- 默认值是空字符串,表示不设置类名,当需要跳过该参数设置第三个default_cmd_action参数时,也可以手动传递空字符串来表示不设置类名
 * 第三个参数default_cmd_action也是可选的,表示在执行完运行时错误回调函数后,是否还需要执行默认的输出错误信息到命令行的动作,
 * 		- 默认值为1,表示需要执行默认动作,如果脚本回调函数里已经将错误信息输出到了命令行的话,就可以将该参数设置为0,表示不需要再执行默认动作了
 */
ZL_EXP_VOID main_builtin_fatal_error_callback(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	const ZL_EXP_CHAR * func_name = "bltFatalErrorCallback";
	ZL_EXP_CHAR * function_name = NULL;
	ZL_EXP_CHAR * class_name = NULL;
	if(argcount < 1)
		zenglApi_Exit(VM_ARG,"usage: %s(function_name[, class_name = ''[, default_cmd_action = 1]])", func_name);
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument [function_name] of %s must be string", func_name);
	}
	function_name = (ZL_EXP_CHAR *)arg.val.str;
	if(strlen(function_name) == 0) {
		zenglApi_Exit(VM_ARG,"the first argument [function_name] of %s can't be empty", func_name);
	}
	// 在设置运行时错误回调函数名之前,先将之前可能设置过的回调函数相关的函数名,类名等重置为默认值,可以防止受到之前设置的影响
	main_fatal_error_reset_to_default_values();
	main_fatal_error_set_function_name(function_name);
	if(argcount > 1) {
		zenglApi_GetFunArg(VM_ARG,2,&arg);
		if(arg.type != ZL_EXP_FAT_STR) {
			zenglApi_Exit(VM_ARG,"the second argument [class_name] of %s must be string", func_name);
		}
		class_name = (ZL_EXP_CHAR *)arg.val.str;
		if(strlen(class_name) > 0) {
			main_fatal_error_set_class_name(class_name);
		}
		if(argcount > 2) {
			zenglApi_GetFunArg(VM_ARG,3,&arg);
			if(arg.type != ZL_EXP_FAT_INT) {
				zenglApi_Exit(VM_ARG,"the third argument [default_cmd_action] of %s must be integer", func_name);
			}
			MainFatalError.default_cmd_action = (int)arg.val.integer;
		}
	}
	zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_INT, ZL_EXP_NULL, 0, 0);
}

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

/**
	用户程序执行入口。
*/
int main(int argc,char * argv[])
{
	..................................................................................................
	if(zenglApi_Run(VM,argv[1]) == -1) { //编译执行zengl脚本
		main_fatal_error_set_error_string(zenglApi_GetErrorString(VM));
		if(main_fatal_error_callback_exec(VM, argv[1], MainFatalError.error_string) == -1) {
			main_exit(VM,"\n执行脚本<%s>的FatalError回调函数失败:\n%s\n", argv[1], zenglApi_GetErrorString(VM));
		}
		else if(MainFatalError.default_cmd_action) {
				main_exit(VM,"错误:编译执行<%s>失败:%s\n", argv[1], MainFatalError.error_string);
		}
	}
	..................................................................................................
	
	return 0;
}

    当执行脚本时如果发生了严重的运行时错误时,如果脚本中通过bltFatalErrorCallback模块函数设置了错误处理回调函数的话,上面就会在main_fatal_error_callback_exec函数中,通过zenglApi_ReUse和zenglApi_Call之类的接口来调用脚本中的错误回调函数,并将具体的错误原因和函数栈追踪信息,以参数的形式传递给该回调函数。这样用户就可以在自定义的脚本函数里处理这些错误信息了,例如可以将错误信息写入到指定的日志文件中等。

    此外,这里bltFatalErrorCallback模块函数所设置的错误回调脚本函数,只会在发生了严重的运行时错误时才会被触发,如果是脚本的语法错误则不会被触发,因为当发生了语法错误时,编译器会直接退出,不会再去调用解释器来执行脚本代码了,因此,bltFatalErrorCallback模块函数也就不会被执行了。

test_fatal_error.zl:

    为了测试bltFatalErrorCallback模块函数,当前版本增加了test_fatal_error.zl测试脚本,该脚本在linux中的文件位置为test_scripts/v1.8.3/test_fatal_error.zl,具体的代码如下:

use builtin;
def TRUE 1;
def FALSE 0;
class Fatal
	fun fatal_error(error, stack)
		print '\n *** hahaha fatal error callback: *** \n  ' + error + '\n *** backtrace: *** \n' + stack + '\n';
	endfun
endclass

bltFatalErrorCallback('fatal_error', 'Fatal', FALSE);

class Test
	fun test()
		a = bltTestHa();
	endfun
endclass

Test.test();

    上面脚本中,通过bltFatalErrorCallback模块函数将Fatal类中的fatal_error方法设置为了错误回调函数,那么在调用Test.test()时,会因调用bltTestHa这个无效的函数而抛出运行时错误,并将错误信息传递给Faltal.fatal_error脚本回调函数去处理,该脚本函数会将错误信息(上面的error参数)以及函数栈追踪信息(上面的stack参数)通过print语句打印出来。此脚本在linux中的执行结果如下:

[root@localhost linux]# ./zengl test_scripts/v1.8.3/test_fatal_error.zl 
run(编译执行中)...
stat cache file: "caches/1_8_3_8_5ac8843522be4f31608f55e029deda82" failed, maybe no cache file [recompile]

 *** hahaha fatal error callback: *** 
  
 err: run func err , function 'bltTestHa' is invalid pc=38 (解释器运行时错误:函数'bltTestHa'无效)

 source code info: [ bltTestHa ] 14:7 <'test_scripts/v1.8.3/test_fatal_error.zl'>

 *** backtrace: *** 
 test_scripts/v1.8.3/test_fatal_error.zl:14 Test:test
 test_scripts/v1.8.3/test_fatal_error.zl:18 


run finished(编译执行结束)
[root@localhost linux]# 

使用Android Studio开发android项目:

    v1.8.3版本的android目录中的android项目是使用Android Studio 4.0.1版本开发的,在安装了ndk插件后,可以直接导入android目录中的项目来编译运行,以下是在Android Studio中打开项目后的截图:

Android Studio中打开项目后的截图

    android项目编译运行后,在手机上的执行情况如下:

test.zl脚本

testerror.zl脚本

    可以看到android可以使用v1.8.3版本的所有功能,例如缓存功能,哈希数组,以及修复过的zenglApi_ReUse接口等。

zenglOX中运行当前版本:

    当前版本的代码可以直接放入zenglOX 3.2.0中运行,只需将当前版本的linux目录和zenglOX目录分别拷贝到zenglOX 3.2.0版本下的build_initrd_img/extra/zengl/目录内,覆盖掉该目录中的linux目录和zenglOX目录,再将当前版本的zenglOX目录中的scripts目录(包含testerr.zl测试脚本)拷贝到zenglOX 3.2.0版本下的isodir目录中即可(isodir里的文件会被拷贝到生成的iso镜像中)。

    在ubuntu 20.04系统中编译运行zenglOX 3.2.0时,需要使用gcc 9.3.0的版本,qemu需要使用5.1.0的版本,如下图所示:

ubuntu 20.04中编译zenglOX所需的gcc和qemu的版本

    qemu 5.1.0版本可以在官网下载,gcc 9.3.0等的下载地址请参考zenglOX栏目的zenglOX 3.2.0版本对应的文章的底部的文章中的相关链接。例如:ftp://ftp.gnu.org/gnu/gcc 中包含了各个版本的gcc的代码,gcc编译所依赖的gmp等的链接也是以此类推。具体的编译方法,请参考zenglOX 3.2.0版本对应的文章的组建交叉编译工具部分。

    当前版本在zenglOX 3.2.0中的执行情况如下:

zengl v1.8.3版本在zenglOX 3.2.0中的执行情况

    需要注意的是,由于ubuntu 20.04中make iso命令生成的zenglOX.iso镜像里的文件名都是小写的,因此,zenglOX 3.2.0版本的isoget程式无法正确执行,该程式是以大写的形式来拷贝iso里的文件的,因此,在ubuntu 20.04中,你只能手动在zenglOX 3.2.0中用file命令来一个个的拷贝文件了(isoget程式是执行多个file命令来实现批量拷贝iso文件的程式)。

结束语:

    我没有时间考虑过去,我只考虑未来。

—— 钱学森

 

上下篇

下一篇: zengl v1.9.0 增加self特殊类名,增加zenglApi_SetDefLookupHandle等接口

上一篇: zengl v1.8.2 修复内存泄漏

相关文章

zengl v1.4.0 调试接口 zengl_SDL项目

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

zengl编程语言v0.0.24,SDL捕获鼠标事件,BUG修复

zengl v1.2.1 修复函数调用BUG

zengl v1.9.1 函数参数可以使用负数作为默认值

zengl v1.3.0 位运算符 字符串脚本解析 缓存 Bug修复