说句实话,如果只是设计做出一门编程语言,并不是一件多复杂的事情,一个人三到六个月足矣,但如果要形成一整套编译,开发,调试的工具链,让这门语言能够解决实际生产力问题而不是玩具,一需要投入大量的时间去开发相关的工具链,二要在长时间的工程实践中去验证去完善什么功能是需要的,而什么功能只是花瓶的玩具功能.对语言进行完善精简.这就是一项庞大的工程了.
多年前我就已经从内存的管理开始,编写过词法分析器-->语法分析器-->编译器-->虚拟机-->调试器,编写过一个完整的类C编译型脚本语言PainterScript,并为其编写了一整套编译开发工具链.作为自己设计的图形游戏引擎功能的补充 , 曾经相当一段长的时间,这门语言的作用仅仅只是证明我也是写过编译器的人了,下次碰上杠精我就能更加有底气地直接把github的地址扔上去,
可惜在项目中,其功能定位尴尬,绝大多数时候只能整个项目框架里吃灰,沦为比上不足比下有余的玩具语言.但随着PainterEngine UI框架的完善和PainterEngine Live2D(LiveFramework)的控制需求,这门玩具语言终于迎来了了柳暗花明又一村的春天,使用了PainterScript恰了几口饭终于成为了生产力的时候,我不禁发出了真香的感叹.
当然,作为一个曾经被C艹的各种规范和语法糖恶心到多年的人,这门语言最开始的设计理念就是,简单,更加简单,无脑,更加无脑,因此,设计这门语言我的一开始的目标就是,关键词能少就尽可能的少,不该出现的奇奇怪怪的语法糖坚决不出现,不需要的功能哪怕现在吹的再香,坚决不用,
最好就是那种十分钟入门,二十分钟上手,三十分钟出货的语言.杜绝心智负担,杜绝无脑吹,杜绝对学习成本的白嫖.
鉴于我本人也是懒出名的实在不想整些花里胡哨的东西以显示自己的标新立异,我认为这些东西除了增加语言的学习成本根本没卵用,因此关键词设计起来和C语言极其相似,这样我就可以直接用visual studio code来直接写代码这样就能白嫖各种令人舒服的语法检查和补全功能,同时对有C语言基础的人,只需要再花十分钟看看,就可以直接上手了
那么这门语言到底是怎么样的呢,我甚至能在这篇回答里写完这个脚本的教程
首先需要搭建执行环境,因为PainterScript就和它的名字一样,是图形/游戏引擎PainterEngine的功能补充,因此语言的执行环境是基于PainterEngine执行的条件下的,关于这个下面有PainterEngine的在windows/android/linux的移植教程
之后就非常简单了,PainterEngine内置了一个Executer Console,只需要不操过六行代码,就可以将脚本编译到执行的环境搭建起来
PainterEngine_Application
.
h
typedef
struct
{
PX_Executer
exe
;
//定义Executer
PX_Runtime
runtime
;
}
PX_Application
;
PainterEngine_Application
.
c
#include
"PainterEngine_Application.h"
PX_Application
App
;
px_bool
PX_ApplicationInitialize
(
PX_Application
*
pApp
,
px_int
screen_width
,
px_int
screen_height
)
{
PX_ApplicationInitializeDefault
(
&
pApp
->
runtime
,
screen_width
,
screen_height
);
PX_ExecuterInitialize
(
&
pApp
->
runtime
,
&
pApp
->
exe
);
//初始化Executer
//加载script.ps脚本文件,编译然后执行
PX_ExecuterRunScipt
(
&
pApp
->
exe
,(
px_char
*
)
PX_LoadFileToIOData
(
"script.ps"
).
buffer
);
return
PX_TRUE
;
}
px_void
PX_ApplicationUpdate
(
PX_Application
*
pApp
,
px_dword
elpased
)
{
//更新控制台
PX_ExecuterUpdate
(
&
pApp
->
exe
,
elpased
);
}
px_void
PX_ApplicationRender
(
PX_Application
*
pApp
,
px_dword
elpased
)
{
px_surface
*
pRenderSurface
=&
pApp
->
runtime
.
RenderSurface
;
PX_RuntimeRenderClear
(
&
pApp
->
runtime
,
PX_COLOR
(
255
,
255
,
255
,
255
));
//渲染控制台
PX_ExecuterRender
(
&
pApp
->
exe
,
elpased
);
}
px_void
PX_ApplicationPostEvent
(
PX_Application
*
pApp
,
PX_Object_Event
e
)
{
PX_ApplicationEventDefault
(
&
pApp
->
runtime
,
e
);
//处理I/O消息
PX_ExecuterPostEvent
(
&
pApp
->
exe
,
e
);
}
如果成功运行,在windows中,你会看到这样的界面
当然,鉴于PainterEngine的全平台支持特性,移植时代码你一个字母都不需要改,使用Android NDK重新编译后在你的Android手机上运行,绝对没一点毛病
现在,就可以打开script.ps文件,开始写PainterEngine Script 代码了,为了证明这个十分钟入门,二十分钟上手,三十分钟出货的口号不是在吹牛逼,当然我也不指望这门脚本语言有多少人用(毕竟自己用的爽,才是这门语言的最终目的),但花个十分钟瞧一瞧看一看你也买不了吃亏买不了上当,何况PainterEngine Script从词法分析语法分析到编译虚拟机的代码都是完整开源的,如果你是正在发愁今年毕业设计做什么的大四本科僧,你要看得上尽管拿去用,捞个院优校优毕设,应该不成问题,老规矩,仍然从Hello World开始
Hello PainterScript
#name "main"
#include
"stdio.h"
export
int
main
()
{
print
(
"Hello PainterScript"
);
}
首先,PainterScript的开头必须是 #name "名称" 的格式,用来表示这个代码的"文件名",之所以这么做是因为PainterEngine的移植平台有可能是没有文件系统的嵌入式环境,必须要有一个标识来记录这个代码文件的名称,然后你就可以使用#include "名称"的方式,将这个源代码包含进来.这和C语言的#include本质上大同小异,只是C语言的#include很可能要求你的编译平台需要有文件系统的支持
之后的int main()仍然和C语言一样,是Executer的入口函数,也就是说第一行代码从这里执行,之后就是使用Print函数将文本打印出来了.
PainterScript 基本数据类型
PainterScript有且仅有4中基本基础数据类型+其对应的指针类型,当然,数组的声明和使用也和C语言一模一样
分别是 int,float,string,memory
int 和 float就不多说了,和C语言的定义基本一样,int表示整数型,float表示浮点型,运算和隐式转换规则也和C语言一模一样,你可能会问char short double long...也支持么,那只能说抱歉,PainterScript不需要这些玩意,int存储整数,float存储浮点数(遵循IEEE754标准)没了,你可以完全按照C来写.
那么重点就来到了PainterScript的自建类型string和memory了,这两个类型,完全是为了贯彻落实简单,更加简单,无脑,更加无脑的理念而诞生的.
string数据类型
先说说string,你可以直接用一个字符串对其进行赋值
string somestring="PainterScript String";
当然,加法也没有问题,例如
string somestring="PainterScript"+"Hello";
或者
string a="hello";
string b="world";
string c=a+b;
如果说,你想访问字符串的第一个字符怎么办,你可以通过[]对字符串的元素访问,例如
int asc;
string str="abc";
asc=str[0];//'a'的asc码
asc=str[1];//'b'的asc码
asc=str[2];//'c'的asc码
你也可以直接修改它们
string str="abc";
str[0]='x';//xbc
print(str);
当然,PainterScript拥有内存的自适应系统,你可以直接建立一个字符串
string str;
str[0]='a';
str[1]='b';
str[2]='c';
print(str);
最后,你可以使用string这个关键字,将其它类型转换为string
string str;
str="PI="+string(3.14);
print(str);
string _666="six six six "+string(666);
print(_666);
memory数据类型
string数据类型用于存储0结尾的字符串,memory数据类型就是用来存储数据流的,例如下面的代码,可以用于构建一个长度为3字节的0x00,0x01,0x02
memory data= @000102@
在PainterScript中,字符串使用双引号包含,而数据流用两个 @ 符号包含
同样的,data数据类型同样支持加法拼接,例如
memory data;
data=@01@;
data=data+@0203@;
都是合法的
你同样可以通过[]运算符号访问对应的索引字节的值,例如
memory data=@00FF@;
print(string(data[0]));//0
print(string(data[1]));//255
结构体
PainterScript的结构体定义,和C艹的差不多,例如
struct person
{
string name;
int age;
}
export int main()
{
person zhangsan;
zhangsan.name="zhang san";
zhangsan.age=18;
print(zhangsan.name);
print(string(zhangsan.age));
}
好了,基本数据类型说完了,剩下说说语句结构
IF语句
不用多说,和c语言差不多其结构为
if(表达式){}else{}
表达式不为0,则为真,需要注意的是,表达式结果必须是int类型或float类型,如果表达式结果是一个string或memory类型,那么编译会报错,但string类型和memory类型可以使用==,!=比较运算符直接进行比较,例如
string a="abc";
a=="abc";//真
a=="aaa";//假
下面是一个简单的脚本比较密码的程序
string a=gets();
if(a=="123456")
print("correct");
else
print("wrong");
while语句
还是和C语言一毛一样
while(表达式){执行语句;}
表达式不为0,则为真,执行语句块代码
int i=6;
while(i--)
{
print(string(i));
}
for语句
仍然是和C语言一毛一样,就不多说了
for (初始化表达式; 循环条件表达式 ;循环后的操作表达式 ){执行语句;}
switch语句
这个和C语言有点不一样,Switch的Case表达式,可以不是一个常量,而且表达式必须用括号包含,其格式为
switch(表达式1)
{
case (表达式2)
{
执行语句1;
}
case (表达式3)
{
执行语句2;
}
.......
}
该语句将会将表达式1的结果与表达式2..3....逐一比较,如果比较为真则执行对应语句块代码
string a=gets();
switch(a)
{
case ("abc")
{
print("input abc");
}
case ("123")
{
print("input 123");
}
}
其它关键字
break;跳出循环或switch,和c语言一样
return;函数返回,和C语言一样
export;导出函数,如果这个函数需要被C语言调用,应该用export修饰
int()将其它数据类型转换为整数,例如int("123")的结果是123
float()将其它数据类型转换为浮点数,例如float("12.3")的结果是12.3
string()将其它数据类型转换为字符串
memory()将其它数据类型转换为内存数据
_asm{} 内联PainterScript ASM汇编
memlen 计算memory类型的数据长度
strlen 计算string类型的字符串长度
没了.
应用场景
PainterScript的其使用场景一般有以下几个
a.用于保护一些关键的"加密"代码,因为PainterScript作为一个编译型脚本语言,由VM进行执行,其逆向难度将大大增加.
b.用于一系列性能要求不高的逻辑控制,因为执行环境可控,能能避免代码导致的内存和其它致命错误问题,能够提高系统的稳定性,
c.用的最多的地方是,用于平台无关的协程或线程实现,例如下面的一个常见的工控场景
COM_Write(@010102@);//使用串口发送一个命令到设备
Sleep(1000);//等待1秒让设备响应
memory data=COM_Read();//从串口读取数据
if(data==@000000@)
{
print("设备成功执行命令");
}
else
{
print("命令执行失败");
}
Sleep是一个阻塞函数,当PainterScript的虚拟机执行到这个阻塞函数后,将返回,这个时候,程序就可以去处理UI绘制之类的其它工作了
第二个是多线程
int Thread1()
{
//do something
}
int Thread2()
{
//do something
}
int Thread3()
{
//do something
}
在工控场景中,各个线程由PainterScript VM负责线程调度,实现并发"多线程"
你可能会问了,为什么要用那么麻烦的方式实现协程或多线程呢?
问得好!
1.C语言本身没有协程和多线程,要实现要么需要平台支持,奇怪的写法要编译器支持
2.一份代码,我一个字都不想改就想在windows/linux/andorid/iOS哪怕是裸奔的单片机上跑
3.个人非常非常懒,甚至不想写平台兼容代码,是的,只需要一个ANSI C 编译器哪怕没有任何的标准库支持,PainterScript都可以一个字都不用改重新编译即可完成移植,PainterEngine 是一个无文件系统依赖,无内存管理依赖,无编译器特性相关,无平台依赖就可以完成移植的引擎,何况裸片的工控嵌入式几乎也没有操作系统平台
4.某语言吹快把内存安全和导致的问题吹上天了,你看,用一门脚本语言控制逻辑,内部调度自行gc,哪来的那么多问题,还要啥车轮子?为什么还要折腾自己?有那么多时间学点别的不好么?
5.逼格高啊.
最后点一下题:并不是没有人设计编程语言,相反的,我相信很多的公司或从业很久的业内人士都有可能开发自己的编程语言,但这个语言首先是为了服务自己的需求诞生的,在此基础上,这个语言要有多流行多少人用或者需要些什么牛哄哄的功能,就显得不那么重要了,毕竟不管怎么来,贴切解决了自己的问题的语言,才是一门好语言