zengl v1.2.0的版本(之所以没使用v2.0的版本号,是因为目前还需要很多的测试),该版本已经重写了所有的源代码,采用面向对象方式,将C文件中主要的全局变量和函数(函数以函数指针的形式)写到结构体...

    zengl v1.2.0的版本(之所以没使用v2.0的版本号,是因为目前还需要很多的测试),该版本已经重写了所有的源代码,采用面向对象方式,将C文件中主要的全局变量和函数(函数以函数指针的形式)写到结构体中。对C文件名,以及源码中变量,宏,结构体等命名方式进行了统一处理,对所有内部错误信息进行统一管理。将zengl转变为类似lua的嵌入式脚本语言,zengl是以动态连接库的形式嵌入到用户的C或C++程序中,取消原来的.zlc后缀的汇编指令文件,直接编译执行.zl脚本,编译器和解释器都整合在zengl动态链接库中。

    zengl v1.2.0采用github作为源码管理平台,可以访问https://github.com/zenglong/zengl_language/  查看详情,在该页面右侧有个Download Zip按钮可以下载源码,或者可以通过Fork来copy一份,github提供了强大的在线源码浏览功能,可以直接在浏览器中查看源代码。


图1


图2

    百度盘地址:http://pan.baidu.com/share/link?shareid=3281005547&uk=940392313 该页面是一个rar压缩包,其实是作者在上面的Download Zip按钮下载下来的压缩包解压后再重命名打包为rar格式,所以和github中的代码没有区别。

    sourceforge的项目地址:https://sourceforge.net/projects/zengl/files/  访问该地址可以看到zengl_lang_v1.2.0.rar的压缩包

    windows和linux的代码为了方便管理,放在了一个压缩包中,linux中的程序直接make编译运行即可(需要root权限,因为需要将libzengl.so拷贝到/usr/lib中),里面有usage.txt说明文档,windows的vs2008项目在github同步时自动过滤掉了很多文件,windows_vs2008目录中的zengl_lang_v1.2.0.sln直接双击无法打开,只有进入zenglrun目录或zengl目录,运行里面的zenglrun.vcproj或者zengl.vcproj来打开,另外vs2008里的所有文件都在github同步时由dos换行符转为了unix换行符,好在vs2008打开可以正常浏览和运行,只是直接用记事本打开有点乱,如果觉得不习惯,可以自己用editplus之类的工具转码,还可以将源代码文件拷出来,自己新建一个vs2008,新建vs2008请参照原来的项目配置来做,比如zengl需要配置为动态链接库,预处理器定义部分需要设置ZL_EXP_OS_IN_WINDOWS;ZL_LANG_EN_WITH_CHZL_EXP_OS_IN_WINDOWS这个宏可以防止出现编译错误(下面有解释),ZL_LANG_EN_WITH_CH让zengl输出默认的中英文混合错误信息,如果不想看到烦人的警告信息,可以将禁用特定警告设为4996,4715,对应main.c这个用于测试zengl嵌入式开发的项目,需要设置好调试的命令参数为test.zl ,为了方便调试,可以将main.c所在的项目设为依赖于zengl动态链接库项目:



图3(zengl项目必须是dll动态链接库类型)
 


图4(zengl项目必须设置的预处理器)



图5(zengl项目可以设置禁用相关警告信息)



图6(main.c测试项目需要设置的用于测试的命令参数)



图7(可以将main.c所在的项目设为依赖于zengl,方便调试)



图8(main.c所在项目也可以设置禁用特定警告)
    zengl中主要的源码文件如下:

    zengl_locals.c 国际化语言文件,将虚拟机内部所有可能的出错信息,以及关键字的字符串信息,汇编指令的输出符号信息等都定义在该文件的不同数组中。可以定义自己的出错信息以及该出错信息对应的宏,在编译zengl生成动态链接库时,通过定义ZL_LANG_EN_WITH_CH宏,可以让zengl输出默认的中英文混合出错信息。如果需要定义其他语言的出错信息,可以在该文件中自定义一个ZL_LANG_....之类的宏,再自己写一个ZL_Error_String数组,最后在编译zengl时,指定ZL_LANG_....宏即可。

    zengl_main.c zengl编译器的主体程式,里面定义了和编译器相关的函数,主要是词法扫描相关的函数,当然也包括一些编译器和虚拟机的初始化函数等。

    zengl_parser.c 该文件中定义了一些和语法解析相关的函数。语法解析生成AST抽象语法树的过程主要集中在zengl_express函数中,该函数采用第三个版本的语法解析引擎,对以前版本的算法进行了调整,采用纯状态机加优先级堆栈的方式,比第二个版本的可读性强很多,语法错误的定位也更准确,方便维护和扩展。

    zengl_symbol.c 该文件定义了和符号表相关的函数,全局变量,函数,类等符号信息都是通过该文件中的函数来生成。

    zengl_assemble.c 该文件定义了和汇编指令输出相关的函数,汇编指令不再像以前的版本那样输出到.zlc文件中,而是直接输出到虚拟机解释器的指令动态数组中,这样汇编链接结束后,解释器就可以马上执行。

    zengl_ld.c 该文件定义了和链接器相关的函数,主要是对JMP等指令中的跳转地址进行解析,将汇编中的伪跳转地址转为真实的汇编指令位置。

    zenglrun_main.c 该文件定义了和虚拟机的解释器相关的函数,解释器通过这些函数来解释执行汇编指令。

    zenglrun_func.c 该文件定义了和解释器相关的辅助函数,如解释器中内存池的操作函数等。

    zenglApi.c 该文件定义了zengl虚拟机对外提供的接口函数(即zengl动态链接库对外的导出函数),用户通过这些接口函数就可以在自己的C程序及C++程序中加载执行zengl脚本,并为zengl脚本自定义一些模块函数等。

    zengl_exportfuns.h 该头文件中主要是对zengl动态链接库导出函数进行声明,要在自己的程序中使用zengl,就必须包含该头文件,并且在包含该头文件前,定义操作系统宏,例如windows环境下,需要#define ZL_EXP_OS_IN_WINDOWS宏,然后再包含该头文件,当然也可以在VS之类的IDE的项目配置中定义,或者makefile中定义,这些地方定义了就不需要在源码中再定义了,如果没有这个宏,那么编译就会出错,因为这个宏决定了导出函数是以windows的dll方式导出,还是以Linux的so方式导出。对于Linux系统,可以定义ZL_EXP_OS_IN_LINUX宏,也可以不定义,因为zengl默认就是使用Linux模式。

    zengl_exportPublicDefs.h 该头文件会由zengl_exportfuns.h自动包含进来,里面定义了一些宏和结构体,用户在使用zengl导出的接口时,可能会用到这些宏和结构体,通过阅读该文件,可以知道为什么在windows系统下,一定要定义ZL_EXP_ON_IN_WINDOWS宏的原因。另外该文件中还包含了zengl主次版本号等信息(这些信息以宏的形式存在)。

    zengl_global.h 该头文件是zengl内部的核心头文件,所有虚拟机,编译器,解释器等相关的宏和结构体,以及上面提到过的C源码中需要使用的宏,枚举值,结构体等,都定义在该头文件中。

    zengl_locals.h 该头文件定义了zengl_locals.c中出错信息对应的宏等。

    以上就是zengl动态链接库生成时所需的核心源码文件。

    另外,需要注意的是,main.c并不是zengl的核心文件,它只是用来测试zengl嵌入式开发的测试程序,里面有zengl加载方法,API接口的使用示例等,用户可以参照这个文件来使用zengl进行嵌入式脚本开发,对于C++的用户在定义自己的模块函数时,请将这些模块函数使用extern "C"括起来。

    由于zengl已经转为嵌入式编程语言,所以之前的SDL之类的模块都没包含进来,作者只想保留最核心的编译解析部分,其他的模块函数请在用户自己的程序中完成(包括array这样的函数,用户也必须自己定义,可以参照main.c中的代码,注意: print属于保留关键字,不属于模块函数)。

    增加了类函数,用法可以参考test.zl脚本
    
    作者:zenglong
    时间:2013年8月14日
    官网:www.zengl.com

    通过查看main.c,就可以知道zengl嵌入的方法,就像常规的动态链接库的使用方法一样,首先你的项目需要找到zengl动态链接库的lib,以及zengl对外提供的两个头文件:zengl_exportfuns.hzengl_exportPublicDefs.h ,然后在需要使用zengl的C或C++文件中加载zengl_exportfuns.h头文件,zengl_exportPublicDefs.h会自动由zengl_exportfuns.h包含进来。前面也提到过:在加载头文件前,如果是windows系统就必须设置ZL_EXP_OS_IN_WINDOWS宏,否则会有编译错误,linux系统则不需要定义宏。

    在加载了头文件后,就可以使用zengl动态链接库中对外的导出函数,例如main.c在main入口处通过zenglApi_Load来加载并执行某脚本:

    int len = 0;
    ZENGL_EXPORT_VM_MAIN_ARGS vm_main_args = {main_userdef_info ,
                                              main_userdef_compile_error,
                                              main_userdef_run_info,
                                              main_userdef_run_print,
                                              main_userdef_run_error,
                                              main_userdef_module_init,
                                              ZL_EXP_CP_AF_IN_DEBUG_MODE |
                                              ZL_EXP_CP_AF_OUTPUT_DEBUG_INFO};
    if(argc < 2)
    {
        printf("usage: %s <filename> ... (用法错误,应该是程序名加文件名加选项参数的形式,文件名通常是以.zlc结尾,也可以是.zl结尾)\n",argv[0]);
        exit(-1);
    }

    printf("compiling(编译中)...\n");
    debuglog = fopen("main_debuglogs.txt","w+");
    if(zenglApi_Load(argv[1],&vm_main_args) == -1) //编译执行zengl脚本
    {
        printf("错误:编译<%s>失败!请查看debuglogs.txt分析出错原因!\n",argv[1]);
        #ifdef ZL_EXP_OS_IN_WINDOWS
            system("pause");
        #endif
        exit(-1);
    }

    zenglApi_Load第一个参数是脚本文件名,第二个参数是用户自定义的一些虚拟机配置信息,比如main_userdef_info函数是用户自定义的编译器调试信息输出函数,当使用了ZL_EXP_CP_AF_OUTPUT_DEBUG_INFO调试标记时,编译器在编译生成语法树和符号信息时,会将这些信息通过main_userdef_info这个用户自定义的函数来传递给用户,用户可以在此函数中将信息输出到日志中,或显示出来等等,ZL_EXP_CP_AF_IN_DEBUG_MODE为开启调试模式,建议始终开启调试模式,这样脚本运行出错时就可以定位到错误的位置。main_userdef_run_print为用户自定义的当脚本中调用print指令时的信息输出方式。main_userdef_compile_error,main_userdef_run_error这两个顾名思义就是当编译器和解释器出错时会调用的用户自定义函数。main_userdef_run_info是解释器相关的调试信息所调用的用户自定义函数。main_userdef_module_init为用户自定义的模块初始化函数,用户可以在此函数中为不同的模块设置不同的初始化函数,例如:

ZL_EXP_VOID main_userdef_module_init(ZL_EXP_VOID * VM_ARG)
{
    zenglApi_SetModInitHandle(VM_ARG,"builtin",main_builtin_module_init);
    zenglApi_SetModInitHandle(VM_ARG,"sdl",main_sdl_module_init);
}

    通过zenglApi_SetModInitHandle接口可以设置builtin和sdl模块的初始化函数为main_builtin_module_init及main_sdl_module_init,这样当脚本中使用use关键字加载builtin时,就会自动调用main_builtin_module_init:

ZL_EXP_VOID main_builtin_module_init(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT moduleID)
{
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"printf",main_builtin_printf);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"read",main_builtin_read);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltRandom",main_builtin_random);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"array",main_builtin_array);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltPrintArray",main_builtin_print_array);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltTestAddr",main_builtin_test_addr);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltSetArray",main_builtin_set_array);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltLoadScript",main_builtin_load_script);
    zenglApi_SetModFunHandle(VM_ARG,moduleID,"bltGetZLVersion",main_builtin_get_zl_version);
}

    在该函数中又可以通过zenglApi_SetModFunHandle接口来设置该模块下的自定义的模块函数,例如printf,read等,当zengl脚本中调用printf时,就会自动调用用户自定义的main_builtin_printf函数:

/*builtin模块函数*/
ZL_EXP_VOID main_builtin_printf(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
    ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
    ZL_EXP_INT i;
    if(argcount < 1)
        zenglApi_Exit(VM_ARG,"printf函数的参数不可以为空");
    for(i=1;i<=argcount;i++)
    {
        zenglApi_GetFunArg(VM_ARG,i,&arg);
        switch(arg.type)
        {
        case ZL_EXP_FAT_INT:
            printf("%d",arg.val.integer);
            break;
        case ZL_EXP_FAT_FLOAT:
            printf("%.16g",arg.val.floatnum);
            break;
        case ZL_EXP_FAT_STR:
            printf("%s",arg.val.str);
            break;
        default:
            zenglApi_Exit(VM_ARG,"printf函数第%d个参数类型无效,目前只支持字符串,整数,浮点数类型的参数",i);
            break;
        }
    }
    return;
}

    在main_builtin_printf函数中又通过zenglApi_GetFunArg接口来获取脚本中传递过来的参数,然后根据参数的类型,将参数在终端显示输出。zenglApi_Exit函数是脚本退出函数,可以随时停止脚本的执行,并输出错误信息,这些错误信息就会通过前面vm_main_args中的main_userdef_run_error自定义函数来输出。

    上面提到的zenglApi接口函数在zengl_exportfuns.h头文件中都有声明,声明中带了简单的注释信息,因篇幅就不一一解释。

    在main.c的最下方,有一大段注释,这段注释里的main入口函数使用了其他的更灵活的方法来加载执行zengl脚本。用户可以将这段注释取消掉,在将之前的main函数注释掉(因为一个C文件中不能有两个同名的入口函数),再运行测试。这种方式中先zenglApi_Open()创建一个虚拟机,再用zenglApi_SetFlagszenglApi_SetHandle等设置一些虚拟机选项,还可以直接zenglApi_SetModInitHandle来设置模块初始化函数,zenglApi_SetModFunHandle设置模块函数的处理句柄,不需要额外再创建一个函数。通过zenglApi_Run来运行脚本,运行完后如果有错误信息,可以通过zenglApi_GetErrorString来获取出错信息,在用完虚拟机后,必须用zenglApi_Close来释放掉虚拟机的系统资源。还可以zenglApi_Reset重置之前的虚拟机,接着用重置过的虚拟机继续zenglApi_Run运行其他脚本,在运行完一个脚本后可以通过zenglApi_GetValueAsStringzenglApi_GetValueAsInt,
zenglApi_GetValueAsDouble
来获取脚本中某个全局变量的值,最后还可以zenglApi_Push结合zenglApi_Call来调用某脚本中的某个函数或类函数。

    这两种方法各有利弊,第一种使用zenglApi_Load的方式操作简单,而且不需要手动释放虚拟机等资源,这些资源会在zenglApi_Load执行完时自动释放掉。不过这种方式需要设置较多的自定义函数,而且无法获取脚本中的变量信息,也无法调用脚本中的某个函数。

    使用第二种方式就比较灵活,不过需要手动释放资源,还需要注意Api接口调用的先后顺序,顺序出问题,也会导致各种问题。所以应根据需要来选择使用。

    在上面的模块函数中用到的ZENGL_EXPORT_MOD_FUN_ARG等结构体都定义在zengl_exportPublicDefs.h头文件中,可以浏览该文件来查看详情。

    授之鱼不如授之渔,只有分析调试源代码才能理解原理。限于篇幅就不多说了。

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

下一篇: zengl v1.2.1 修复函数调用BUG

上一篇: zengl编程语言v1.0.6 问号冒号选择运算符,endswitch,endclass

相关文章

v1.3.1 Android编译执行zengl脚本

zengl v1.4.1 及zengl_SDL新增的Android工程

开发自己的编程语言-zengl编程语言v0.0.1词法扫描器的实现

zengl编程语言v0.0.23,SDL游戏开发,类定义

zengl v1.7.3 获取数组之类的内存块中非NONE成员的数量

zengl编程语言 中序