为保持与 Go+ 开发者、爱好者的密切沟通,共同促进、交流 Go+ 的迭代发展,Go+ 开发团队特别策划「Go+ 公开课」系列直播课程。基于 inside Go+ 的视角进行分享,并设置从易至难的练习题,希望帮助大家通过实操的方式进一步深入理解 Go+、参与 Go+ 的贡献。
Go+ 公开课 · 第 5 期,特别邀请 Go+ 贡献者进行主题分享,介绍「Go+ClassFile 原理与实战」。本文内容基于许式伟分享整理,点击文末「阅读原文」可查看完整版视频回放。
直播预告:Go+ 公开课 • 第 6 期
12月9日 20:00
主讲人:Go+ 贡献者 陈东坡
分享主题:如何给 Go+ 贡献代码 & gop.cache 的作用和实现

扫描二维码 预约直播
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
本期公开课内容分上下两部分,第一部分是介绍 ClassFile 的原理,第二部分是实战环节,我们会利用 Go+ 的 ClassFile 做一个语法上类似于 Matlab 的画图引擎。
这也是公开课中的第一次实战课程,我们希望借助这种方式,帮助大家从零开始利用 Go+ 尝试制作具备实用功能的项目,通过实战更好的理解 ClassFile 的特性。本周一,Go+ 发布了最新的 1.0.28 版本,新增支持 gop.mod 功能。ClassFile 引擎的自动注册正是通过 gop.mod 实现的。也希望更多开发者可以通过学习、使用,帮助我们发现潜在的问题,迭代更多的功能。我从去年 2 月份开始参与 Go+ 的开发,基本都是通过周末的时间来贡献代码,目前已经贡献 23 个 PR。另外我还为 Go+ 贡献了一个 vscode 插件,目前 Go+ 团队比较缺乏制作 vscode 插件的工程师,对这部分的同学欢迎联系我。
老许在此前的公开课分享过,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+ 编译过程中扮演的角色。图中信息大致可以分为三部分:- 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:' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
issue 地址:https://github.com/goplus/gop/issues/915
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.17
gop 1.0
classfile ExtGmx, ExtSpx, "pkgPathOfClassFile"
require (
...
)
第二个语句是 ClassFile,这部分语句定义的是 ClassFile 需要支持的扩展名。也就是如果哪个 DSL 语言注册了 ClassFile,编译器要自动为中间代码 import ClassFile 需要依赖的包。module moduleUserProj
go 1.17
gop 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 的解释文件,从而通过解释文件注册到编译器中。Classfile 基本概念:Classfile 有两个基本概念,一个叫主体,另一个叫个体,对主体进行操作的代码写到主体文件中,对个体进行操作的代码,写到个体文件中,主体文件和个体文件,采用不同的扩展名,需要将扩展名定义到 gop.mod 的 classfile 语句中。
主体对象和个体对象:每个 Classfile 必须定义一个主体对象,个体对象可定义和可不定义,编译器在编译的时候,会创建一个以主体文件名为名称的主体结构体,内部会包含主体对象,同时,会创建一个以个体文件名为名称的个体结构体,内部包含个体对象,同时主体对象还会包含所有创建的个体结构体,个体结构体中也会包含主体结构体。代码入口定义:编译器会给主体结构体生成 MainEntry() 方法,主体文件中的语句,会被放到该 MainEntry 方法中,同时给个体结构体生成 Main 方法,个体文件中的语句,会被放到这个 Main 方法中。最后生成 main 函数,并加入 {classfile}.Gopt_{主体对象名称}_Main() 的函数调用。ClassFile 函数名称:当 DSL 语言调用了某一个 xxx 函数时,编译器回去 classfile 引擎中查找是否包含了该函数,如果未包含,则去查找主体对象下是否包含了该函数,如果再未找到,则去查找是否有 Gopt_{主体对象名称}_xxx 为名称的函数。ClassFile 需要根据自己的需要的入参去选择合适的定义方式。
上文中我们提到 ClassFile 和 DSL 模块的定义。通过 gop mod init 进行模块的初始化,生成 gop.mod。对 gop.mod 修改后,增加上图中最后一段语句,也就是 ClassFile 中需要说明的几个关键字,比如需要支持的扩展名「.p」;个体文件因为在这个例子中没有使用到,因此我们用“空”来代替。最后的这段语句,便是当 DSL 语言使用到 ClassFile 引擎时,编译器需要自动为中间代码 import 的 package。上图是 DSL 部分,需要去注册一个 ClassFile。因为示例为本地测试,所以采用的是 replace 方法,让 ClassFile 重新指向一个本地的 ClassFile。这个行为和 go.mod 的 replace 行为是一致的。上图是常规的“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”。linspace 是 Matlab 的一个语句,我们这里实际上是仿照 Matlab 实现 ClassFile 内部的函数。然后通过 Go+ 的 for 循环进行 y 的 x 平方计算,最终通过 plot 函数输出函数曲线。上图是 Go+ 编译过程的中间代码。大家可以发现,所有的代码都被定义到 MainEntry 中。前文中“Hello world”示例是通过 ClassFile.say 调用 ClassFile 中定义的函数,该示例中则出现了三种情况的调用:同样是通过函数调用,但中间代码中为什么会出现不同的形式?这需要看一下 ClassFile 的定义。首先,ClassFile 定义了主体对象 Figure,入口函数跟随变更。main 对象中的 index 不能直接调用 MainEntry 的原因,便是因为需要对主体对象先进行初始化。在初始化时,需要对 index 和 Figure 进行初始化,然后才能无异常的调用 plot。示例中我们通过 Figure 对象构建了一个坐标,因此我们需要先对坐标以及 UI 引擎进行定义。初始化函数会对坐标函数以及 UI 引擎进行初始化,对坐标进行绘制并将绘制后的图片输出到 UI 引擎中。操作符重载是在实践过程中发现很重要的部分。在此前的例子中,我们便将 Matlab 中没有内置的赋值方式进行了改写。因为 Go+ 暂时不支持向量的运算,所以需要对诸如乘法操作符等进行操作符重载。具体实现的过程如下:- 如果有,则将 x * x 改为 x.Gop_Mul(x)
上图代码中大家可以发现,此前的 this.Plot 变成了 this.Plot__1,这是 Go+ 中支持的函数重载。下图中我们对向量的加减乘除都进行了操作符重载的定义。前面生成的中间代码中我们发现,plot 生成的中间代码 由 this.Plot 变成了 this.Plot__1,因为 plot 由原来的入参 []float64 变更为 Vector,因此为了让我们的 plot 支持多种入参方式,我们把各种入参可能性都做了定义,编译器会自动选择最为合适的函数来调用。- 对于函数名称以 __x 结尾的函数(x可以为数字或字母),我们称为重载函数;
熟悉 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 中包含的坐标。我们可以将这几个例子拆解成两部分:同时,我们需要对 gop.mod 进行一个新的定义:var 中包含的是需要绘制的两个坐标,坐标文件的名称通过个体对象文件的文件名获取;run(1,2)的意思是我们需要绘制一个「一行两列」的坐标组。大家可以发现主体结构定义生成的结构体中,即包含主体文件对象,还包含了刚刚定义好的两个坐标的对象。其中,每个坐标对象还内置了 ClassFile 定义的坐标对象,并内置了主体的 index 结构体。与此前示例不同的是,编译器为每个坐标生成了一个 Main 方法,所有的个体文件中的代码也都被包含到相应的坐标函数中。要想运行即包含主体对象又包含个体对象的函数,只需要让 Gopt_Figure_Run 函数执行坐标一与坐标二的 Main 函数即可。因此,Run 的具体实现可改写成如下形式:首先,通过反射的方式拿到 index 的结构体,通过结构体的每一个 Field 判断是否包含 Main 函数。如果包含,则调用。通过 subplot 函数定义坐标具体绘制的位置,最终把生成的坐标赋值给 Align。这样,我们就实现了通过 ClassFile 的个体对象的特性,来绘制的坐标组。
- 支持 xlabel、ylabel、title、legend
https://github.com/go-wyvern/gplothttps://github.com/go-wyvern/gplot-tutorial
Go+ 开发团队会协助大家解决训练过程中遇到的问题。联系方式如下:
1. Go+ 用户群(微信群):可以直接在群里提出问题,团队会直接在社群进行解答;2. Go+ 公开课(知识星球):本次演讲的 PPT 及文字内容会同步在知识星球中,欢迎在上面提问交流。