cover_image

吴信谊:Go+ ClassFile 原理与实战丨Go+ 公开课 • 第 5 期

Go+ 工作组 七牛云技术团队
2021年12月07日 09:07
图片

为保持与 Go+ 开发者、爱好者的密切沟通,共同促进、交流 Go+ 的迭代发展,Go+ 开发团队特别策划「Go+ 公开课」系列直播课程。

基于 inside Go+ 的视角进行分享,并设置从易至难的练习题,希望帮助大家通过实操的方式进一步深入理解 Go+、参与 Go+ 的贡献。

Go+ 公开课 · 第 5 期,特别邀请 Go+ 贡献者进行主题分享,介绍「Go+ClassFile 原理与实战」。

本文内容基于许式伟分享整理,点击文末「阅读原文」可查看完整版视频回放。




直播预告:Go+ 公开课 • 第 6 期

12月9日 20:00

主讲人:Go+ 贡献者 陈东坡

分享主题:如何给 Go+ 贡献代码 & gop.cache 的作用和实现


图片

扫描二维码 预约直播




往期回顾:

第 1 期:Go+ v1.x 的设计与实现
第 2 期:import 过程与 Go+ 的模块管理
第 3 期:Go+ STEM 引擎基础以及动画机制
第 4 期:Scratch vs. Go+ Spx STEM 引擎详细对比



图片


本期公开课内容分上下两部分,第一部分是介绍 ClassFile 的原理,第二部分是实战环节,我们会利用 Go+ 的 ClassFile 做一个语法上类似于 Matlab 的画图引擎。

 
这也是公开课中的第一次实战课程,我们希望借助这种方式,帮助大家从零开始利用 Go+ 尝试制作具备实用功能的项目,通过实战更好的理解 ClassFile 的特性。
 
本周一,Go+ 发布了最新的 1.0.28 版本,新增支持 gop.mod 功能。ClassFile 引擎的自动注册正是通过 gop.mod 实现的。也希望更多开发者可以通过学习、使用,帮助我们发现潜在的问题,迭代更多的功能。
 
我从去年 2 月份开始参与 Go+ 的开发,基本都是通过周末的时间来贡献代码,目前已经贡献 23 个 PR。
 
图片
 
另外我还为 Go+ 贡献了一个 vscode 插件,目前 Go+ 团队比较缺乏制作 vscode 插件的工程师,对这部分的同学欢迎联系我。
 
下面正式开始今天的分享。
 


 

一. ClassFile 原理

 
老许在此前的公开课分享过,Go+ 是一门面向连接的语言。
 
图片
 
大家通过屏幕上的图片可以发现,Go+ 连接了非常多的 DSL。DSL 是领域专用语言,英文全称为 Domain Specific Language。领域专用语言是用来解决特定领域中特定任务的计算机语言。相比于通用语言,DSL 的开发相对容易,但开发的步骤是一致的,同样包含词法分析、抽象语法树构建、编译等等。
 
假设借助 Go+ 的 ClassFile 引擎构建 DSL 语言,开发者则不需要关心语言开发本身的过程,只需去定义领域的专有知识,这样便可大大降低开发的门槛,帮助开发者轻松搞定一门 DSL 语言。
 
Go+ 除了能够定义一门 DSL 语言,也可以定义、运行多门 DSL 语言。因此,Go+ 天然支持多门 DSL 语言的交叉。
 
有很多的科学家与学者其实研究的是交叉学科,同时面向多个领域。若能借助 Go+ 这种支持交叉编程的语言或工具,可能会出现意想不到的化学现象。
 
那么,Go+ 具体如何定义 DSL?我们先举一个例子。
 
图片
 
上图中时通过 Go+ 的 ClassFlie 绘制出的一条 SinX 曲线。观察该图可以发现几个问题:
 
  • 为什么扩展名是 .plot 不是 .gop,gop 是如何做到可以运行 .plot 的代码?
  • linspace 和 plot 并不是 Go + 的内置函数,是如何做到函数调用的?
  • 它是通过 Go+ 的 ClassFile 引擎做的,那它是如何跟 ClassFile 引擎建立联系的?
  • linspace 返回的是一个向量,Go+ 是否支持向量计算?
 
带着这四个问题,我们先接着往下看。
 
 

1. ClassFile 在 Go+ 编译过程中扮演的角色

 
图片
 
上图描述的主要是 ClassFile 在 Go+ 编译过程中扮演的角色。图中信息大致可以分为三部分:
 
  • 源码。也就是 DSL 语言的源码;
  • ClassFile。ClassFile 专门用来定义 DSL 语言所需要的接口;
  • 编译器相关模块组件。包含抽象语法树、编译器、模块管理、ClassFile 引擎等。
 
DSL 语言代码在实际运行过程中,首先模块管理会读取源码中的 gop.mod 文件,这个文件会对源码进行一个描述,如果是 DSL 语言便会注册一个 ClassFile。注册的 ClassFile 被模块管理识别后,模块管理会下载 ClassFile 的 package,下载后读取 package 中的 gop.mod 文件(该文件中包含该 ClassFile 具体实现的描述)。
 
同时,模块管理还会将 ClassFile 引擎注册,注册后 ClassFile 引擎便会包含该 ClassFile 的基本信息。
 
在编译的时候,抽象语法树会读取这个 ClassFile 引擎,获取该引擎支持的扩展名。这样抽象语法树便可对源码进行解析,得到抽象语法树。编译器同时也会读取 ClassFile 引擎,并针对 ClassFile 的工作模式进行特殊的处理。
 
我们刚才提到,DSL 语言跟 ClassFile 间的关联基本通过 gop.mod 实现,那么 ClassFile 具体的设计规格是如何的?
 
Go+ 的官网目前暂时还没有具体的 ClassFile 设计规格文档,但老许此前提过一个 issue:
 

图片

issue 地址:https://github.com/goplus/gop/issues/915

 
这个 issue 中包含两个部分:
 
  • ClassFile 的 gop.mod 定义
  • DSL 语言的 gop.mod 定义

首先我们来看 ClassFile 的定义。
 
import "github.com/goplus/gop/cl"
cl.RegisterClassFileType(ExtGmx, ExtSpx, "pkgPathOfClassFile")

该部分主要相比于 go.mod 增加了几个语句,也就是 gop.mod 1.0。也就是说 ClassFile 是基于 gop.mod 1.0 进行的开发。
 
module moduleOfClassFile
go 1.17gop 1.0
classfile ExtGmx, ExtSpx, "pkgPathOfClassFile"
require ( ...)

第二个语句是 ClassFile,这部分语句定义的是 ClassFile 需要支持的扩展名。也就是如果哪个 DSL 语言注册了 ClassFile,编译器要自动为中间代码 import ClassFile 需要依赖的包。
 
module moduleUserProj
go 1.17gop 1.0
register moduleOfClassFile // add by `gop mod download pkgPathOfClassFile`
require ( pkgPathOfClassFile vX.X.XX)
 
这部分是 DSL 语言部分的 gop.mod。该部分会注册相应的 ClassFile 目录。
 
Go+ 模块管理部分会读取 DSL 语言上的 gop 文件,通过解析获取注册的 ClassFile 的引擎目录,通过 gop.mod 下载相应的 ClassFile。下载完成后再解析 ClassFile 中的 gop.mod,通过解析得到 ClassFile 的解释文件,从而通过解释文件注册到编译器中。
 
 

2. ClassFile 相关概念定义

 

Classfile 基本概念:Classfile 有两个基本概念,一个叫主体,另一个叫个体,对主体进行操作的代码写到主体文件中,对个体进行操作的代码,写到个体文件中,主体文件和个体文件,采用不同的扩展名,需要将扩展名定义到 gop.mod 的 classfile 语句中。

 
主体对象和个体对象:每个 Classfile 必须定义一个主体对象,个体对象可定义和可不定义,编译器在编译的时候,会创建一个以主体文件名为名称的主体结构体,内部会包含主体对象,同时,会创建一个以个体文件名为名称的个体结构体,内部包含个体对象,同时主体对象还会包含所有创建的个体结构体,个体结构体中也会包含主体结构体。
 
代码入口定义:编译器会给主体结构体生成 MainEntry() 方法,主体文件中的语句,会被放到该 MainEntry 方法中,同时给个体结构体生成 Main 方法,个体文件中的语句,会被放到这个 Main 方法中。最后生成 main 函数,并加入 {classfile}.Gopt_{主体对象名称}_Main() 的函数调用。
 
ClassFile 函数名称当 DSL 语言调用了某一个 xxx 函数时,编译器回去 classfile 引擎中查找是否包含了该函数,如果未包含,则去查找主体对象下是否包含了该函数,如果再未找到,则去查找是否有 Gopt_{主体对象名称}_xxx 为名称的函数。ClassFile 需要根据自己的需要的入参去选择合适的定义方式。
 




二. ClassFile 实战

 

1. 模块初始化

 
图片
 
上文中我们提到 ClassFile 和 DSL 模块的定义。通过 gop mod init 进行模块的初始化,生成 gop.mod。对 gop.mod 修改后,增加上图中最后一段语句,也就是 ClassFile 中需要说明的几个关键字,比如需要支持的扩展名「.p」;个体文件因为在这个例子中没有使用到,因此我们用“空”来代替。
 
最后的这段语句,便是当 DSL 语言使用到 ClassFile 引擎时,编译器需要自动为中间代码 import 的 package。
 
图片
 
上图是 DSL 部分,需要去注册一个 ClassFile。因为示例为本地测试,所以采用的是 replace 方法,让 ClassFile 重新指向一个本地的 ClassFile。
 
这个行为和 go.mod 的 replace 行为是一致的。
 
 

2. ClassFile 定义

 
图片
 
上图是常规的“Hello world”示例,通过 say “Hello world ”的方式,打印到标准输出当中。
 
图片
 
上图是编译器生成的中间代码。除了 say(“hello world”)部分,基本全是编译器自动添加的。
 
这部分内容的结构看似繁琐,但实际尝试去自己写 ClassFile 后,就觉得这么设计是非常科学的。
 
图片
 
上图便是为 say(“hello world”)代码所设计的 ClassFile。其中定义的 Gopt_Speak_Main 便是该 ClassFile 的入口,会在该函数中调用MainEntry 方法,来执行我们的 DSL 代码。最后在 MainEntry 中调用 say 函数,通过 fmt.Println 实现标准输出。
 
上图中浅蓝色部分是我们定义的几个常量。其中 GopPackage = true 代表该 ClassFile 是 Go+ 的一个包;gop_game = "Speak" 的意思是该 ClassFile 的主体名称是 “Speak”。


3. 类 Matlab 画图组件

 
图片
 
linspace 是 Matlab 的一个语句,我们这里实际上是仿照 Matlab 实现 ClassFile 内部的函数。然后通过 Go+ 的 for 循环进行 y 的 x 平方计算,最终通过 plot 函数输出函数曲线。
 
图片
 
上图是 Go+ 编译过程的中间代码。大家可以发现,所有的代码都被定义到 MainEntry 中。
 
前文中“Hello world”示例是通过 ClassFile.say 调用 ClassFile 中定义的函数,该示例中则出现了三种情况的调用:
 
  • ClassFile.Linspace
  • this.plot
  • math.Pi
 
同样是通过函数调用,但中间代码中为什么会出现不同的形式?这需要看一下 ClassFile 的定义。
 
图片
 
首先,ClassFile 定义了主体对象 Figure,入口函数跟随变更。main 对象中的 index 不能直接调用 MainEntry 的原因,便是因为需要对主体对象先进行初始化。在初始化时,需要对 index 和 Figure 进行初始化,然后才能无异常的调用 plot。
 
示例中我们通过 Figure 对象构建了一个坐标,因此我们需要先对坐标以及 UI 引擎进行定义。
 
图片
 
初始化函数会对坐标函数以及 UI 引擎进行初始化,对坐标进行绘制并将绘制后的图片输出到 UI 引擎中。
 

4. 操作符重载

 
图片
 
操作符重载是在实践过程中发现很重要的部分。在此前的例子中,我们便将 Matlab 中没有内置的赋值方式进行了改写。
 
因为 Go+ 暂时不支持向量的运算,所以需要对诸如乘法操作符等进行操作符重载。具体实现的过程如下:
 
  • 编译 binary 表达式,如 x * x
  • 获取 x 类型
  • 判断 x 是否具有 Gop_Mul 的方法
  • 如果有,则将 x * x 改为 x.Gop_Mul(x)
 
生成的中间代码如下图所示:
 
图片
 
上图代码中大家可以发现,此前的 this.Plot 变成了 this.Plot__1,这是 Go+ 中支持的函数重载。
 
下图中我们对向量的加减乘除都进行了操作符重载的定义。
 
图片
 
 

5. 函数重载

 
图片
 
前面生成的中间代码中我们发现,plot 生成的中间代码 由 this.Plot 变成了 this.Plot__1,因为 plot 由原来的入参 []float64 变更为 Vector,因此为了让我们的 plot 支持多种入参方式,我们把各种入参可能性都做了定义,编译器会自动选择最为合适的函数来调用。
 
函数重载具体的实现过程如下:
 
  • 对于函数名称以 __x 结尾的函数(x可以为数字或字母),我们称为重载函数;
  • 编译器会查找重载函数中符合条件的函数,并且使用。
 
 

6. 类 Matlab 画图组件中「子坐标」功能

 
图片
 
熟悉 Matlab 的朋友知道,Matlab 是通过 subplot 函数来绘制子坐标。第一个参数是需要绘制的坐标组的行数,第二个参数为绘制的坐标组的列数,第三个参数为当前需要绘制的坐标处于的位置,因此第一个坐标绘制的是 sin(x),第二个坐标绘制的是 cos(x)。
 
下图是编译器生成的中间代码:
 
图片
 
因为调用了 subplot 函数,所以需要在 Figure 主体对象中定义 subplot 方法。区别于 Figure 中只有一个坐标的方法,这个示例中需要把 Figure 进行重新定义。
 
图片
 
该示例中 Figure 对象需要支持的是二维数组的坐标体,pos 定义的是当前 Figure 正在绘制的是第几幅坐标;w、h 代表每个坐标的长度和所占的像素多少;rows 和 cols 用来定义坐标的行和列数。下图是 Subplot 的实现:
 
图片
 
将 Figure 对象重新定义后,对旧函数也需要进行重新改写。此前是坐标组写法,因此需要对坐标组中的坐标进行绘制。
 
图片
 
然后将绘制结果保存到 convas 对象中,通过 convas 对象输出 Image。最后是使用 ClassFile 个体对象的特性来绘制子坐标。
 
因为 ClassFile 不仅包含主体对象,可能还包含个体对象。在 Matlab 引擎中,主体对象就是 Figure,个体对象就是 Figure 中包含的坐标。我们可以将这几个例子拆解成两部分:
 
图片
绘制的 sin(x) 图片
 
图片
绘制的 cos(x) 图片
 
同时,我们需要对 gop.mod 进行一个新的定义:
 
图片
 
主体对象中文件的写法可修改成如下;
 
图片
 
var 中包含的是需要绘制的两个坐标,坐标文件的名称通过个体对象文件的文件名获取;run(1,2)的意思是我们需要绘制一个「一行两列」的坐标组。
 
下图为 ClassFile 生成的中间代码:
 
图片
 
大家可以发现主体结构定义生成的结构体中,即包含主体文件对象,还包含了刚刚定义好的两个坐标的对象。
 
其中,每个坐标对象还内置了 ClassFile 定义的坐标对象,并内置了主体的 index 结构体。与此前示例不同的是,编译器为每个坐标生成了一个 Main 方法,所有的个体文件中的代码也都被包含到相应的坐标函数中。
 
要想运行即包含主体对象又包含个体对象的函数,只需要让 Gopt_Figure_Run 函数执行坐标一与坐标二的 Main 函数即可。因此,Run 的具体实现可改写成如下形式:
 
图片

首先,通过反射的方式拿到 index 的结构体,通过结构体的每一个 Field 判断是否包含 Main 函数。
 
如果包含,则调用。通过 subplot 函数定义坐标具体绘制的位置,最终把生成的坐标赋值给 Align。这样,我们就实现了通过 ClassFile 的个体对象的特性,来绘制的坐标组。




三、练习题

 
  • 支持 xlabel、ylabel、title、legend
  • 支持 line style、grid 等其他特性
  • 支持更多 Plotters,如饼图、热图
 
练习题参考资料:
https://github.com/go-wyvern/gplot
本文中的代码资料:
https://github.com/go-wyvern/gplot-tutorial



Go+ 开发团队会协助大家解决训练过程中遇到的问题。联系方式如下:
 
1. Go+ 用户群(微信群):可以直接在群里提出问题,团队会直接在社群进行解答;
2. Go+ 公开课(知识星球):本次演讲的 PPT 及文字内容会同步在知识星球中,欢迎在上面提问交流。



点击左下角「阅读原文」
回看公开课完整视频内容
↙↙↙
继续滑动看下一个
七牛云技术团队
向上滑动看下一个