ZBScript C++脚本编译器
日期:2010年8月5日
作者:张博
1 介绍
ZBScript是一个脚本编译程序,可以将类似简单C++语言函数的脚本编译执行,可以嵌入任何C++程序。
大型软件系统中为了增加系统的可扩展性,通常都会引入脚本解释器或脚本编译器执行用户定义的脚本。大型脚本解释器如JavaScript的开源产品功能强大但过于庞大,而各种小型脚本解释器则又存在功能局限和语法差异,而且语法多数并非采用类C++语法,也少见支持plugin(外挂函数)的。
基于上述原因,本人开发了ZBScript脚本解释器,支持C++语法和plugin,在实践中证明是一个C++项目的功能扩展的较好解决方案。
2 功能特点
2.1 简单移植
提供一个C++头文件,包含入任何支持STL的C++源文件即可,纯粹的标准C++代码,无需任何配置、无需任何特殊编译选项。
任何环境:UNIX、Windows、Linux
任何编译器:Sun CC、HP CC、IBM xlC、MS CPP、G++
一句话引用:#include “ZBScript.h”
2.2 标准C++语法(脚本语言)
脚本语言采用标准C++语法,目前的支持度为大致相当于C语言的单个函数体。即编写一个脚本和编写一个C语言函数相似。因此对于C++程序员来说基本无需学习,这将极大地方便使用过C++的用户编写脚本。
本脚本编译器除了增加了string类型外不会对C++做其它扩展。随着功能的逐渐完善,本脚本编译器未来也许可以直接解释真正的C++源代码
2.3 强数据类型
与大多数脚本不同,本系统严格遵循C++的规范,不支持数值和字符串的自动转换。
变量也必须提前定义。
2.4 C++运行库支持(内部函数)
大多数已经实现为内部函数,包括各种字符串操作、数值转换操作。
2.5 支持plugin(外部函数)
外部功能通过支持CFunction接口接入脚本编译器。Plugin是C++源代码中编写的函数,而不是脚本。
通过插件脚本可以调用系统的任何功能——只要编写一个函数即可。
2.6 编译执行
由于脚本先编译后执行,因此可以预先检查脚本语法,并且可以获得比解释执行高得多的执行速度。
3 脚本语法详解(脚本编写者)
3.1 对C++标准的支持
脚本语法为C++的子集,以及少量扩展。
关键字遵循C++标准,即使尚未支持也禁止用作标识符。
标识符、常量、转义字符符合C++标准
运算符、优先级、结合性符合C++标准
数据类型的自动转换符合C++标准
For、do、while、if、else、break、continue、return符合C++标准
3.2 语言扩展
增加string,类似STL::string,代替char[]
运算符“+”可用于连接字符串
取消字符类型,字符串常量可以用双引号也可以用单引号
允许不明确使用return,默认返回最后一条语句的值
3.3 语法说明
"未支持的关键字、运算符全部保留,不可用做标识符/r/n"
"/r/n"
"已经支持的语法:/r/n"
"语句:if else return {} for do while break continue;/r/n"
"转义字符:C++标准/r/n"
"注释://到行尾/r/n"
"/r/n"
"增加的关键字:string/r/n"
"/r/n"
"强数据类型:/r/n"
"整数:int long/r/n"
"浮点数:float double/r/n"
"字符串:string/r/n"
"数值常量:C++标准/r/n"
"字符串常量:双引号单引号均可/r/n"
"不允许数值和字符串的自动化转换/r/n"
"算术运算+ - * / % ++ --/r/n"
"逻辑运算! > < >= <= == !=/r/n"
"关系运算&& ||/r/n"
"赋值运算= += -= *= /= %=/r/n"
"逗号运算,/r/n"
"+可用作字符串连接/r/n"
"++ --前缀每个变量只能用一次,后缀只能独立使用/r/n"
"/r/n"
"每个声明语句只能声明一个变量,可初始化/r/n"
"for语句的初始化子句不能声明变量/r/n"
"/r/n"
"最后执行的表达式结果作为返回值,包括循环的判断语句/r/n"
"空语句返回空/r/n"
"if语句若无匹配语句返回空/r/n"
"break continue不影响返回值/r/n"
"声明语句返回变量值/r/n"
"复杂逻辑下应使用return语句/r/n"
3.4 内部函数清单
abs : 返回值 DOUBLE
绝对值,1个参数,参数必须是数值
ceil : 返回值 DOUBLE
向上取整,1个参数,参数必须是数值
floor : 返回值 DOUBLE
向下取整,1个参数,参数必须是数值
max : 返回值 DOUBLE
取最大值,1-N个参数,参数必须是数值
min : 返回值 DOUBLE
取最小值,1-N个参数,参数必须是数值
nullfun : 返回值 NULLVARIABLE
测试用函数,用户不可调用
round : 返回值 LONG
四舍五入,1个参数,参数必须是数值,负数向0舍入,-1.2返回-1,-1.8返回-2
strcat : 返回值 STRING
字符串连接,2个参数,参数必须是字符串
strcmp : 返回值 LONG
字符串比较,2个参数,参数必须是字符串
stricmp : 返回值 LONG
字符串忽略大小写比较,2个参数,参数必须是字符串
strlen : 返回值 LONG
字符串长度,1个参数,参数必须是字符串
strncmp : 返回值 LONG
字符串部分比较,3个参数
参数1 字符串
参数2 字符串
参数3 整数
strnicmp : 返回值 LONG
字符串忽略大小写部分比较,3个参数
参数1 字符串
参数2 字符串
参数3 整数
strstr : 返回值 LONG
查找子串,2个参数,参数必须是字符串,返回参数2在参数1中出现的位置,若没找到则返回-1
substr : 返回值 STRING
获取子串,2-3个参数,参数必须是数值,参数2为开始位置,参数3为字符数,省略参数3则一直取到字符串结束
to_double : 返回值 DOUBLE
转换为浮点数,一个参数,任意类型
to_long : 返回值 LONG
转换为整数,一个参数,任意类型
to_string : 返回值 STRING
转换为字符串,一个参数,任意类型
4 编程指南(C++程序员)
4.1 引入包含文件
在需要使用脚本功能的源代码中加入:
#include “ZBSCript.h”
4.2 使用变量Variable
Variable是内部使用的数据类型,支持所有的基本数据类型的赋值和比较
包含4种类型:空,整数,浮点数,字符串
任何时候只有一种类型,根据type的不同,数据存储在lValue、dValue或strValue之中
不应该直接使用lValue、dValue或strValue
应该使用赋值来设置变量的值、使用isXXXX函数来确认数据类型、使用GetXXXX函数来获得指定类型的值(不会改变变量的类型)
Clear将变量清为空
Initvalue将变量的三个值清空,但不改变类型
4.3 使用CZBScript
4.3.1 声明
zbstd_script::CZBScript zbs;
4.3.2 编译
bool Compile(char const * _source,CZBVector<pair<string,Variable > > * pVars=NULL)
_source为脚本源代码
pVars指向外部变量集合,为“变量名”-“变量值”对的形式,变量名必需符合C++规范,变量值必需提供类型,建议同时提供初值。
外部变量视同脚本语句定义的变量,编译时不需要提供有意义的变量值,仅需要变量名来编译脚本。
如果存在语法错误返回false,可以用GetMessage获得错误信息。
4.3.3 添加外部函数
CFunctionMap实现了全局的外部函数表,使用此对象的全局方法AddFunction即可添加外部函数到全局外部函数表:
static bool AddFunction(char const * name,Variable::types type,CFunction*p);
name为函数名,必须符合C++函数名规范
type为返回值类型
p指向函数对象,外部函数如何编写见后续章节
在编译脚本前必须完成添加所有必须的外部函数,否则编译会失败
4.3.4 执行
bool Execute(Variable & ret,CZBVector<pair<string,Variable > > * pVars=NULL,void * pe=NULL)
ret返回脚本执行的结果。
pVars指向外部变量,变量类型已经在编译时确定,执行时仅仅赋值。
pe参数直接传递给外部函数,一般可以传递一个指向外部特定对象的指针以供外部函数处理
如果存在执行错误返回false,可以用GetMessage获得错误信息。
注意区分脚本返回值和本函数的返回值。本函数返回true的情况下脚本返回值ret才有意义。
4.3.5 杂项
string GetMessage()const
获得出错信息
string const & GetSource()const
获得当前脚本
bool IsCompiled()const
判断是否成功编译,取决于之前的Compile是否成功
string & Report(string & ret)
输出脚本编译后结构,可用于检查脚本的逻辑错误
4.4 编写plugin(外部函数)
4.4.1 继承CFunction接口
Plugin必需实现CFunction接口,该接口实现了插件的统一管理功能。
4.4.2 构造CFunction接口
CFunction(char const * _name,Variable::types _type)
参数为函数名和返回值类型
4.4.3 显示函数说明
virtual string & help(string & ret)
plugin必须实现此接口来提供函数说明,至少包括函数的参数、返回值等信息。默认的实现仅提供了返回值说明
此函数对于外部管理程序在运行时提供交互式的帮助是很有意义的。脚本解释器并不调用此函数。
4.4.4 编译时检查
virtual bool Check(CZBVector<Variable > & params,void * & pc,string & msg)
此函数在脚本编译时调用。
Params由编译函数Compile传入,包含函数的每个参数
pc引用与外部函数在脚本里的一次使用相关的指针,该指针也会作为函数执行的参数。该指针在编译时由编译器创建,与外部函数的一次使用相关联。该参数指向的数据可以由Check函数建立,根据特定的函数参数情况建立信息,然后在函数执行时根据这些信息作处理。一个典型的用法是外部插件已经使用了独立的参数格式,使用pc构造一个符合外部插件需要的参数对象。
如果检查不通过,msg返回错误信息。
4.4.5 执行函数功能
virtual bool Exec(CZBVector<Variable > & params,void * & pc,Variable & ret,string & msg,void * pe)
params由脚本执行程序传入,为函数本次执行的参数。这些参数可能是变量,每次执行都不同。
Pc与Check函数的pc参数相同,引用同一个与函数的一次引用相关的指针。如果Check函数将这个指针指向了一个有效数据结构,该结构此时传递给函数使用。
Ret返回函数执行结果。
Pe由脚本执行程序的参数给出,直接传递给函数。通常为函数执行所需的外部数据。
如果执行失败,msg返回出错信息。
注意区分函数返回值和本调用的返回值,仅当本调用返回true的时候ret才有意义。任何一个函数调用失败(本调用返回false)都将导致整个脚本执行失败(得不到脚本返回值)。
5 脚本示例(脚本编写者)
示例1 简单表达式:
123+4.5*3-max(1,2,3,0)
示例2 简单逻辑:
if(12>5)12+5;else 12*5
示例3 复杂逻辑,注意确保每个执行路径都返回有意义的结果:
int i;//int与long同义
double d=1.5+max(1,2,3);//float与double同义
string s=to_string(d);
do //do循环
{
++i;
}while(i<10);
while(i>0) //while循环
{
i-=d;
}
for(i=0;i<5;++i) //for循环
{
if(strlen(s+'abc')>=3) //if语句 if()...或if()...else...或if()...else if()...else...可以有多个else if
{
continue;
}
else
{
break;
}
}
return i+d;
6 编程示例(C++程序员)
6.1 简单调用
如果不使用环境变量,仅需要下面代码中红色字体的三行
//声明脚本对象和环境变量组,环境变量组不是必需的
zbstd_script::CZBScript * zbscript=new zbstd_script::CZBScript;
vector<pair<string,zbstd_script::Variable > > envs;
//添加了一个环境变量dis_total,类型为整数
pair<string,zbstd_script::Variable > tmppair;
zbstd_script::Variable tmpvar;
tmpvar.type=zbstd_script::Variable::LONG;
tmppair.first="dis_total";
tmppair.second=tmpvar;
envs.push_back(tmppair);
//执行编译,如果不需要环境变量可省去第二个参数
if(!zbscript->Compile(str,&envs))
{
MessageBox(zbscript->GetMessage().c_str(),"出错",MB_OK|MB_ICONSTOP);
}
//设置环境变量的值
envs[0].second=10L;
//执行脚本并显示结果
zbstd_script::Variable var;
if(!zbscript->Execute(var,&envs))
{
MessageBox(zbscript->GetMessage().c_str(),"出错",MB_OK|MB_ICONSTOP);
}
else
{
MessageBox(var.GetString().c_str(),"运行结果");
}
6.2 编写外部函数
可参考内部函数,下面是一个内部函数max的代码,这个函数可以这样使用
max(1,3,-1),结果为3,因为3是三个参数中最大的
该函数实现为CMax类
Check函数检查是否至少有一个参数并且所有参数都是数值类型
Exec函数把所有参数转换为double类型比较大小,返回最大的
pc和pe参数没有使用
struct CMax : public CFunction
{
CMax():CFunction("max",Variable::DOUBLE){}
virtual string & help(string & ret)
{
ret=CFunction::help(ret);
ret+="取最大值,-N个参数,参数必须是数值/r/n";
return ret;
}
virtual bool Check(CZBVector<Variable > & params,void * & pc,string & msg)
{
msg="";
if(params.size()<1)msg+="参数不足/r/n";
for(size_t i=0;i<params.size();++i)
{
if(!params[i].isNumber())msg+="参数必须是数值/r/n";
}
return 0==msg.size();
}
virtual bool Exec(CZBVector<Variable > & params,void * & pc,Variable & ret,string & msg,void * pe)
{
size_t _max=0;
for(size_t i=1;i<params.size();++i)
{
if(params[i].GetDouble()>params[_max].GetDouble())_max=i;
}
ret=params[_max];
return true;
}
};
6.3 为外部plugin实现CFunction接口
6.3.1 已经存在的外部plugin接口
class BasePlugin
{
public:
virtual STATE init(){return 0;}
virtual STATE execute(const VParam& params,CEventPacket& inputPacket,long& retValue) = 0;
virtual STATE post(){return 0;}
}
这是一个常见的plugin接口,init对插件初始化,execute执行插件,post做插件清理。
Init和post通常由外部系统调用,需要用脚本解释器调用的是execute函数。
Execute函数的参数params是一个参数列表,大致相当于vector<string>,inputPacket是函数要处理的对象,retValue获得返回值。从这个函数的参数可以看出脚本解释器的pc和pe参数的必要性,pc可以实现外部plugin接口的params,pe则用来传递inputPacket。
6.3.2 扩展外部plugin接口
将外部插件基类增加一个父类CFounction,然后实现CFunction:
class BasePlugin : public zbstd_script::CFunction
{
public:
virtual STATE init(){return 0;}
virtual STATE execute(const VParam& params,CEventPacket& inputPacket,long& retValue) = 0;
virtual STATE post(){return 0;}
public://下面是zbstd_script::CZBScript::CFunction接口插件不要再重载
//由于外部插件接口不包含自说明接口,help无法实现,仍调用了默认的help函数
virtual string & help(string & ret)
{
ret=zbstd_script::CFunction::help(ret);
return ret;
}
//Check使用pc建立了外部接口的参数组,然后对参数做了检查(代码已省略)
virtual bool Check(CZBVector<zbstd_script::Variable > & params,void * & pc,string & msg)
{
pc=new VParam;
VParam * pVparam=(VParam *)pc;
pVparam->reserve(params.size());
pVparam->resize(params.size());
。。。。。。检查参数。。。。。。
return true;
}
//Exec向pc建立的外部参数组(由pluginparams引用)传入了实际的参数值,将pc还原为inputPacket,然后调用execute
virtual bool Exec(CZBVector<zbstd_script::Variable > & params,void * & pc,zbstd_script::Variable & ret,string & msg,void * pe)
{
VParam & pluginparams=*(VParam *)pc;
//处理参数
{
for(size_t i=0;i<params.size();++i)
{
pluginparams[i].value=params[i].GetString();
}
}
CEventPacket * pPacket=(CEventPacket *)pe;
long tmp=0;
STATE tmpret;
if(0==(tmpret=execute(pluginparams,*pPacket,tmp)))
{
ret=tmp;
return true;
}
else
{
thelog<<"插件执行出错插件名"<<getName()<<" 出错码"<<tmpret<<ende;
ret=0L;
return false;
}
}
};
6.3.3 注册外部函数
在外部插件管理器注册插件时同时注册外部函数到插件编译器:
bool PluginManager::registerPlugin(const string& pluginName,BasePlugin* plugin)
{
if(_plugins.find(pluginName)!=_plugins.end())
{
thelog<<"插件["<<pluginName<<"]已经存在"<<ende;
return false;
}
_plugins[pluginName] = plugin;
//注册到脚本解释器
if(!zbstd_script::CFunctionMap::AddFunction(pluginName.c_str(),zbstd_script::Variable::LONG,plugin))
{
thelog<<"插件["<<pluginName<<"]注册到脚本解释器失败"<<ende;
return false;
}
return true;
}
6.3.4 编译、执行脚本
完成注册外部函数后就可以编译、执行脚本。