使用过“框架”的同学都能感受到“框架”带来的方便。所谓”框架“本质上就是一系列代码安排帮助我们完成脏活累活,或者复杂的工作流程后,把处理结果交给我们提供的代码。本节我们要完成的 c 语言模板也是一个框架,它也需要做一系列脏活累活,例如读取代码文件里面的内容,然后交给词法识别模块进行处理。其中还有一部分“复杂工作流程”,那就是识别语言对应的正则表达式,将其构建成 DFA 状态机,形成跳转表,将跳转表转换为基于 C 语言的二维数组和对应的调用代码,这些工作则是我们前面在 GoLex 工作中完成的内容,我们需要将对应的二维数组拷贝到本节要创建的 c 语言模板中,当状态机针对输入的字符串完成识别工作后,需要把结果提交给我们提供的处理代码,这些代码就是 input.lex 文件中定义在正则表达式后面的 action 代码。
我们要完成的 c 语言模板代码,首先需要具备的功能是将字符串从文件中读取,然后将其提交给状态机识别,因此我们的第一步工作就是完成一个基于 c 语言的输入缓存模块,由于词法解析需要频繁的将字符串从文件中进行读取,因此输入缓存模块的设计对整个词法解析的应用效率有很大影响,我们希望输入缓存模块能实现如下几点要求:
1,模块对应的函数执行速度要尽可能快,这就需要尽可能少的去对字符串进行拷贝操作。
2,要能支持多个字符的预读取和回退
3,要能支持一定长度的字符串读入
4,要能随时访问当前识别的字符串和上次已经识别好的字符串。
5,尽可能减少磁盘文件读取
为了实现上面约束,我们把缓冲区的设置如下:
如上图所示,Start_buf 是指向缓冲区开头的指针, END 是指向缓冲区末尾的指针。我们在缓存数据时,不能将整个缓冲区全部用完,其中 Start_buf 到 End_buf 这段区域用来存储数据,End_buf 到 END 这段区域没有使用,也就是这段区域会浪费掉。整个缓冲区的大小用 BUFSIZE 表示,用于存储数据的部分大小为 3*MAXLEN,其中 MAXLEN 表示我们一次能识别的字符串长度,Next 指向下一个要读取字符在缓冲区中的位置。Danger 是一个特定标志位,如果 Next 越过了 Danger,那意味着缓冲区中的有效数据已经很少,此时我们需要从磁盘中读取新的数据放入缓冲区。
输入系统的复杂在于,我们需要很多指针来指向不同特定部位,除了上面已经设置的指针外,我们还需要几个指针,他们分别指向上次读取的字符串起始地址和结束地址,当前正在读取的字符串的起始地址,具体情况如下图所示:
如上图所示,pMark 指向上次已经识别的字符串的开始,sMark 指向上次已经识别的字符串的末尾,它同时也是当前正在识别字符串的开始,eMark(对应上图第二个 sMark,我画错了) 指向当前识别字符串的末尾。你可能有疑问为何 Next 指针会在 e
Mark 后面呢,那是因为我们会预读取若干字符,如果这些预读取的字符需要重新放回缓冲区,那么我们直接将 Next 指针调整到 sMark 所在位置即可。
如果 Next 指针所在位置越过了 Danger,那么我们就需要从磁盘读入新的数据。首先我们需要把 pMark 到 End_buf 这段区间的数据往前挪动,直到到 pMark 抵达 Start_buf 所在位置,挪动所产生的新区间就可以让磁盘读入的数据进行填充,具体操作如下图所示:
通常情况下,磁盘读取和数据挪动的开销比较大,一般情况下这种情况出现次数很少,因此不会对程序运行效率产生太大影响。下面我们进入到实现细节,首先我们启动一个空的 c 语言工程,然后在里面创建一个新文件名为 input.c,输入代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include "debug.h"
#include <unistd.h>
#define COPY(d, s, a) memcpy(d, s, a)
//默认情况下字符串从控制台读入
#define STDIN 0
//最大运行程序预读取 16 个字符
#define MAXLOOK 16
//允许识别的最大字符串长度
#define MAXLEX 1024
//定义缓冲区长度
#define BUFSIZE ((MAXLEX * 3) + (2 * MAXLOOK))
//缓冲区不足标志位
#define DANGER (End_buf - MAXLOOK)
//缓冲区末尾指针
#define END (&Start_buf[BUFSIZE])
//没有可读的字符
#define NO_MORE_CHARS() (Eof_read && Next >= End_buf)
typedef unsigned char uchar;
//存储读入数据的缓冲区
PRIVATE uchar Start_buf[BUFSIZE];
//缓冲区结尾
PRIVATE uchar *End_buf = END;
//当前读入字符位置
PRIVATE uchar *Next = END;
//当前识别字符串的起始位置
PRIVATE uchar *sMark = END;
//当前识别字符串的结束位置
PRIVATE uchar *eMark = END;
//上次已经识别字符串的起始位置
PRIVATE uchar *pMark = NULL;
//上次识别字符串所在的起始行号
PRIVATE int pLineno = 0;
//上次识别字符串的长度
PRIVATE int pLength = 0;
//读入的文件急病号
PRIVATE int Inp_file = STDIN;
//当前读取字符串的行号
PRIVATE int Lineno = 1;
//函数 mark_end()调用时所在行号
PRIVATE int Mline = 1;
//我们会使用字符'\0'去替换当前字符,下面变量用来存储被替换的字符
PRIVATE int Termchar = 0;
//读取到文件末尾的标志符
PRIVATE int Eof_read = 0;
/*
后面Openp 等函数直接使用文件打开,读取和关闭函数
*/
int ii_newfile