SQLite编译器部分浅析
0 前言
SQLite是一款C语言实现的轻型数据库,相较于传统的MySQL等数据库,其具有灵活快速,高可靠性,功能齐全的特点。SQLite是世界上最常用的嵌入式数据库引擎,内置于世界上绝大多数手机和其他设备中。本篇将着重于编译器的实现,及其与虚拟机(Virtual Machine)的联系,旨在借此使读者对于SQL内核和编译器部分的运行形成一个初步的认识。
1 SQLite架构概览
如图所示,SQLite 由三个主要模块组成:内核、编译器、后端,下面对三个模块八个部分进行简要介绍。
1.1 公共接口(Interface)
SQLite库大部分公共接口由main.c
,legacy.c
和vdbeapi.c
源文件中的函数来实现,比较常用的函数有sqlite3_open()
,sqlite3_close()
,sqlite3_exec()
等等,可以通过包含头文件"sqlite3.h"
进行调用,之后会通过一个具体的例子进行说明。
1.2 编译器(Compiler)
编译器共包含三部分,词法分析器(Tokenizer)、语法分析器(Parser)、代码生成器(Code Generator)。SQLite对SQL语句采用解释执行的方式,每读入一条语句,Tokenizer和Parser对其进行语法检查,然后生成语法树方便VDBE代码生成器使用,然后把语法树传给VDBE代码生成器(Code Generator)处理。代码生成器根据语法树生成VDBE的类汇编语言操作码。 最后传入Virtual Machine进行执行。
1.2.1 词法分析器
当执行一个包含SQL语句的字符串时,接口程序要把这个字符串传递给tokenizer。Tokenizer的任务是把原有字符串分割成一个个标识符(token),并把这些标识符传递给解析器。Tokenizer是用手工编写的,在C文件tokenize.c
中。
1.2.2 语法分析器
Parser的工作是在指定的上下文中赋予标识符具体的含义。SQLite的语法分析器使用Lemon LALR(1)分析程序生成器来产生,Lemon做的工作与YACC/BISON相同,但它使用不同的输入句法,这种句法更不易出错。驱动Lemon的源文件可在parse.y
中找到。
1.2.3 代码生成器
语法分析器在把标识符组装成完整的SQL语句后,就调用代码生成器产生虚拟机代码,以执行SQL语句请求的工作。代码生成器包含许多文件:attach.c, auth.c, build.c, delete.c, expr.c, insert.c, pragma.c, select.c, trigger.c, update.c, vacuum.c, where.c
。expr.c
处理SQL中表达式的代码生成。where.c
处理SELECT、UPDATE和DELETE语句中WHERE子句的代码生成。文件attach.c, delete.c, insert.c, select.c, trigger.c, update.cvacuum.c
处理同名SQL语句的代码生成(这些文件在必要时都调用expr.c
和where.c
中的例程)。所有其他SQL语句的代码由build.c
生成。文件auth.c
实现sqlite3_set_authorizer()的功能。 同时在SQL语句的代码生成器中也会完成对其相应的语义检查,譬如delete.c
中的sqlite3IsReadOnly()
的作用就是检查以确保给定的表是可写的,反之会传出一条错误消息。
1.3 虚拟机(Virtual Machine / Virtual Database Engine)
代码生成器生成的代码由SQLite中专门处理数据库虚拟机(VDBE)来执行。VDBE全称为Virtual Database Engine,是一个专为操作数据库文件而设计的抽象计算引擎。它有一个存储中间数据的存储栈,每条指令包含一个操作码和不超过三个额外的操作数。
1.4 B-树(B-Tree)
一个SQLite数据库使用B-树的形式存储在磁盘上,B-树的实现位于源文件tree.c
中。数据库中的每个表和索引使用一棵单独的B-树,所有的B-树存放在同一个磁盘文件中。文件格式的细节被记录在btree.c
开头的备注里。B-树子系统的接口在头文件btree.h
中定义。
1.5 页处理器(Pager)
B-树模块以固定大小的数据块形式从磁盘上读取/修改信息,默认的块大小是1024个字节,但是可以在512和65536个字节之间变化。页处理器负责读、写和缓存这些数据块,并且提供事物回滚、事物提交,管理数据文件的锁定等功能。B-树从页处理器中请求特定的页,当它想修改页面、想提交或回滚当前修改时,它也会通知页处理器。页处理器的实现代码在pager.c
中。页处理器接口在头文件pager.h
中定义。
1.6 OS接口(OS Interface)
为了保证SQLite在不同操作系统中的可移植性,如Unix,Windows等,SQLite在OS接口中对其支持的各种操作系统进行了抽象化的实现,如Unix使用os_unix.c
,Windows使用os_win.c
等。每个特定操作系统的实现通常都有自己的头文件,如os_unix.h, os_win.h
。
文件名称 | 主要功能 | 文件名称 | 主要功能 |
---|---|---|---|
main.c |
SQLite Library的大部分接口 | vdbeapi.c |
虚拟机提供上层模块调用的API实现部分 |
legacy.c |
sqlite3_exec() |
vdbe.c |
虚拟机的主要实现部分 |
table.c |
sqlite3_get_table(), sqlite3_free_table() |
vdbe.h |
定义了VDBE的接口,和VDBE中的指令,即struct VdbeOP |
prepare.c |
sqlite3_prepare() |
vdbeaux.c |
vdbe.h 的实现 |
tokenize.c |
词法处理器的实现,sqlite3GetToken() |
btree.h |
定义了B-Tree提供的操作接口 |
parser.c |
语法分析处理器的实现,由parser.y 和Lemon自动产生,sqlite3Parser() |
btree.h |
B-Tree的主要实现 |
parser.h |
语法分析器内部定义的关键字,由parser.y 和Lemon自动产生 |
pager.h |
定义Pager提供的接口 |
update.c, delete.c, insert.c, trigger.c, attach.c, select.c, where.c, vacuum.c, pragma.c, analyze.c |
处理相应的同名SQL语句 | pager.c |
Pager模块的主要实现 |
expr.c |
处理SQL语句中的表达式 | os.h |
定义了为OS Interface之前模块所以提供的操作函数 |
alter.c |
实现ALTER TABLE功能 | os_win.c, os_unix.c, os_os2 |
对应操作系统的OS接口 |
build.c |
处理以下语法:CREATE TABLE, DROP TABLE, CREATE INDEX, DROP INDEX, BEGIN TRANSACTION, COMMIT, ROLLBACK | sqlite3.h |
SQLite的头文件,定义了提供给其他应用使用的接口和数据结构 |
func.c |
实现SQL语句的函数语句 | sqliteInt.h |
定义了SQLite内部使用的接口和数据结构 |
date.c |
与日期和时间转换有关的函数 |
2 SQLite编译器简析
2.1 Lemon介绍
该部分旨在通过对Lemon的简单介绍来帮助读者对生成的语法分析器中的API有一个初步的认识,并且了解Lemon的工作机理。
Lemon是一款用C语言编写的LALR(1)类型语法分析器的生成器,其功能和特性与YACC/BISON类似,但是它要求的.y
语法文件采用与YACC/BISON不同的语法。同时Lemon也具有简便灵活的特性,它的源码仅仅只有lemon.c
中简短的4000余行代码。
通常运行lemon生成器时,先通过编译指令gcc lemon.c
生成lemon.exe
,然后需要两个输入文件:语法文件(.y
) 、语法模板文件(lepar.c
)。但通常只需要给出语法文件,如grammar.y
,而不需要给出模板文件,因为Lemon自带一个设计完备的模板文件(lepar.c
),不过理论上可以另行设计其他模板文件,以满足特殊要求。
在生成lemon.exe
的文件目录下,放入语法文件,如grammar.y
,在命令行中执行命令lemon grammar.y
即可生成如下三个后缀分别为.c, .h
的文件,.c
文件即为语法分析器文件,.h
文件是为每一个终结符分配一个整数的头文件。
需要注意的是Lemon生成的语法分析器,并不是一个完整的程序,仅仅生成可为其他过程调用的一些子函数。其中的主要函数有ParseAlloc(), Parse(), ParseFree()
。下面给出进行语法分析时的核心部分的代码以辅助理解各函数的作用:
Parser() {
*Parser pParser = ParseAlloc(malloc);
while( GetTOken( pString, &Tokenid, &Token) ) {
Parse(pParser, Tokenid, Token);
}
Parse(pParser, 0, Token);
ParseFree(pParser, free);
}
首先,使用ParseAlloc()
创建一个指向分析器Parse
(相当于LALR(1)文法分析中符号栈)的指针pParser
。
然后,对于输入的每一条SQL语句,通过while()
循环,不断通过GetNextToken()
对输入的符号流进行分割处理获取属性字,需要注意的是,GetNextTOken()
并不是自动生成,而是手动编写的,如在SQLite中手动编写了tokenize.c
文件。每次GetNextToken()
都把相应的属性字的类放到终结符栈TOkenid
中,把属性字的值放入Token
中。
随之,将pParser, Tokenid, Token
送入Parse()
函数,进行语法分析。在上述while循环中,一直进行着先取得符号类和符号值,后送进分析器pParser
的操作,直至到达符号流pString
的末尾。
接着,以Parser(pParser, 0, Token)
的形式,最终调用一次Parser()
函数,以便结束语法分析。这一过程必不可少,因为必须要让Tokenid
为0作为结束符(相当于LALR(1)文法分析中符号栈中最后要放入结束符#)。
最后,调用ParseFree()
函数,释放语法分析器和由它所占用的空间。
不过对于Lemon比较有意思的一点在于,通过命令行加-c
选项处理后,可以得到描述语法分析器中各状态以及相应的移进规约动作的.out
文件,下图是一个用lemon简单实现的计算器的状态表,其中shift
代表移进动作,reduce
代表规约动作,accept
代表接受状态,shift、reduce
后的数字代表跳转到的相应状态。个人认为这对于LALR(1)文法的学习是一个很有意思的工具。
2.2 语法分析器
SQLite对SQL语句采用解释执行的方式,并不像C语言编译器先生成AST再对AST进行语义检查、代码生成,相反,当SQLite识别出规约状态后即调用内部的yy_reduce()
函数,完成规约动作,同时对识别出的相应关键词,调用相应的代码生成器函数,如输入SQL语句为CREATE TABLE EXAMPLE
,则在yy_reduce()
中会调用sqlite3StartTable()
函数,并执行相应的语义检查。当然这一过程涉及到相当多的中间函数,在之后的部分会对其核心过程进行阐明,并以一个简单的例子辅助说明。
分析源码可以发现,传入相应的代码生成器函数的并非是传统的AST,而是在<sqliteInt.h>
定义好的Parse结构体,其结构较为复杂,核心成分为一个指向当前数据库的指针db
,指向出错信息表的指针zErrMsg
,指向虚拟机VDBE的指针pVdbe
,详细定义可在附件中的<sqliteInt.h>
中搜索struct Parse
进行查询。
2.3 SQL编译器执行流程
下面通过调用<sqliteInt.h>
中的API实现一个具体的例子,来帮助读者对于SQLite函数调用的过程有一个初步的认识。
#include <stdio.h>