尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VC6.0老工程接入SQLite3:从编译到稳定运行实践

VC6.0老工程接入SQLite3:从编译到稳定运行实践 简介针对Visual C 6.0环境编译的SQLite3数据库引擎资源包面向仍需要维护老版本Windows软件或想深入理解SQLite3内部机制的开发者。压缩包内含从官网获取的SQLite3源码、VC6.0工程工作空间与调试符号文件、API参考文档以及用于验证功能的静态库与动态库测试示例整体仅5.4MB其中工作空间和符号文件可在VC6.0中直接打开并用于代码追踪与编译调优。该版本由作者在VC6.0下完成编译生成的.lib和.dll文件可直接供旧版开发环境链接调用省去自行配置C编译器与构建流程的麻烦。目前已有216人学习下载适合需要在老工具链中嵌入轻量级数据库的团队在维护遗留系统或升级历史项目时尤为实用。同时也可用于阅读源码、自定义编译选项配合文档和测试用例进行调试与性能优化的学习场景。 VC6.0 老工程接入 SQLite3从编译库到稳定运行的一线实践先说点实话Visual C 6.0 这玩意是 1998 年的编译器20 多年了但现在工控现场、老教学系统、企业内部维护中的 MFC 项目依然大量存在。这些项目通常是能跑就不动可一旦有数据存储需求Access 太脆弱MySQL 又要装服务端这时候 SQLite3 反而成了最现实的选择。我也看过不少人在网上问 sqlite3 vc6.0 怎么搭、怎么编译、怎么调用说明这需求一直都有。本文就把我在 VC6.0 下接入 SQLite3 的完整过程、踩坑记录、调优手段都翻出来给同样被困在老环境里的朋友一份可以照着抄的作业。1. 老编译器、老工程为什么还要硬啃 SQLite31.1 真实使用场景不是怀旧是没得选很多人一听到 VC6.0 第一反应是这年代还在用。但真实情况是不少设备厂商的二次开发包只提供 VC6 时代的 API或者客户现场有一批跑了十几年的 MFC 上位机程序源代码和依赖库全锁定在 VC6 环境里。把整个工程升级到 VS2019涉及界面库、第三方控件、通信协议栈的重编译成本根本不是小团队能承担的。在这些老工程里加一个本地数据库SQLite3 几乎是唯一不折腾的选择。它不需要安装服务、不需要配置账号密码、单文件即拷即用整个数据库就是一个 .db 文件备份和迁移都极其简单。SQLite3 的核心引擎全部写在 sqlite3.c 这一个文件里C 语言实现编译进 VC6 工程完全可行这正是它在老环境中能生存的根本原因。1.2 版本选择别一上来就抓最新版这是我在实际项目里确认过的问题VC6.0 的 C 编译器对 C89 标准支持较好但头文件和库体系停留在上世纪SQLite3 官方早已不再针对 VC6 做专门适配。早期版本3.6.x、3.7.x 时代编译几乎没有障碍而新版3.4x 之后的源码里可能会引用stdint.h等 VC6 不存在的头文件或者使用较新的 C 语法特性直接编译会报一堆莫名其妙的错误。我的建议是如果只是给老工程做本地数据存储功能需求不复杂优先选用 SQLite3 的amalgamation 版本3.7.14.1 或 3.6.23.1这两个版本功能稳定、API 齐全对 VC6 的支持最友好。如果确实需要新版特性比如更高并发下的 WAL 性能优化那得做好手工维护一套兼容头文件的准备比如自备一个简化版stdint.h。对大多数工控和桌面工具场景3.6/3.7 时代的能力绰绰有余。2. 编译 sqlite3.lib 是最关键的一步VC6 下两个方案2.1 方案一把 sqlite3.c 直接塞进自己的工程最简单粗暴的方式把sqlite3.c和sqlite3.h从 amalgamation 包里解压出来直接添加到 VC6 的工程里参与编译。这种方式的优点是省去了生成独立 lib 的步骤缺点是每次编译整个工程时都要把 sqlite3.c 重新编译一遍拖慢构建时间。在工程设置里要注意一点sqlite3.c 是纯 C 代码如果你的工程是 C 项目VC6 默认根据文件扩展名决定编译方式.c文件会按 C 编译这一点问题不大。但如果你图省事把sqlite3.c改名成.cpp加入工程那就等着被 C 的严格类型检查折磨吧。SQLite3 源码在设计时考虑了 C 兼容性但直接按 C 编译仍然容易遇到一些类型转换警告和错误老老实实保留.c扩展名才省心。2.2 方案二单独生成静态库我更推荐的方式是单独建一个静态库工程专门编译 SQLite3产出sqlite3.lib再让主工程链接它。这样主工程代码干净编译速度快库文件还能被多个子项目复用。步骤很简单用 VC6 新建一个Win32 Static Library工程工程名随意比如SQLite3Lib。把sqlite3.c添加进工程把sqlite3.h也拷到工程目录便于管理。打开 Project SettingsC/C 选项卡Category 选择 Preprocessor在 Preprocessor definitions 里根据需要添加宏。如果不确定先保持默认不加任何宏也能编过。把 C 语言编译选项里的Program Optimization设为Disable (Debug)或Maximize Speed (Release)看自己需求。F7 编译成功后会在输出目录生成sqlite3.lib。生成 lib 之后把sqlite3.h和sqlite3.lib复制到主工程的公共依赖目录在主工程里通过#pragma comment(lib, sqlite3.lib)或者在 Project Settings 的 Link 选项卡里指定库文件即可。这里有一个我反复提醒过自己的问题Debug 版和 Release 版的 lib 不要混用VC6 的运行时库在两种配置下是不同的混用会出现一堆难以排查的内存错误。2.3 不用 DLL省掉一堆部署麻烦网上也有不少教程建议编译成sqlite3.dll主程序动态加载。这种方案在普通 Windows 环境下没什么问题但在老工控机上就不太靠谱了现场往往对系统目录管控严格DLL 丢失或被杀毒软件拦截的情况很常见。静态链接 sqlite3.lib 后SQLite3 的代码直接进到 exe 里部署时只需要拷贝一个 exe 加一个 db 文件少一个运行时依赖就少一类故障。我自己的经验是在老环境里能用静态库就坚决不用 DLL。3. 接入主程序从 open 到事务批量插入的完整写法3.1 最基础的打开、建表、关闭先看一个最简流程演示 VC6 下如何用 C 代码操作 SQLite3。注意 VC6 不支持//行注释不VC6 的 C 编译器其实支持//注释只是如果你严格开启 ANSI C 模式才会有问题。我习惯写/* */以免某些老工程设置下报错。#include stdio.h #include stdlib.h #include sqlite3.h #pragma comment(lib, sqlite3.lib) int main(void) { sqlite3 *db NULL; char *errMsg 0; int rc; /* 打开数据库文件不存在会自动创建 */ rc sqlite3_open(D:\\data\\mydb.db, db); if (rc ! SQLITE_OK) { printf(open error: %s\n, sqlite3_errmsg(db)); return -1; } /* 建表 */ rc sqlite3_exec(db, CREATE TABLE IF NOT EXISTS t_user( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER);, 0, 0, errMsg); if (rc ! SQLITE_OK) { printf(create table error: %s\n, errMsg); sqlite3_free(errMsg); sqlite3_close(db); return -1; } printf(database opened and table ready\n); sqlite3_close(db); return 0; }这代码在 VC6 下直接能跑。sqlite3_open接收的是 UTF-8 编码的路径字符串在中文 Windows 的 VC6 工程里如果直接传一个 ANSI 中文字符串字面量会出现路径打不开的问题。上面代码里我用D:\\data\\mydb.db这种纯英文路径就是为了避开编码转换。后面专门说这个问题。3.2 参数绑定别再用 sprintf 拼 SQL 了老工程师写代码有个习惯插入数据喜欢用sprintf拼 SQL 字符串在 SQLite3 里这样做不光有 SQL 注入风险还有一个实际麻烦字符串里的单引号、特殊字符会把 SQL 搞坏。正确方式是使用sqlite3_prepare_v2配合参数绑定。sqlite3_stmt *stmt NULL; const char *sql INSERT INTO t_user(name, age) VALUES(?, ?);; int i; sqlite3_prepare_v2(db, sql, -1, stmt, NULL); for (i 0; i 100; i) { sqlite3_reset(stmt); /* 重置语句准备下一轮绑定 */ sqlite3_clear_bindings(stmt); /* 清掉上一次的绑定值 */ sqlite3_bind_text(stmt, 1, 张三, -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 2, 20 i); if (sqlite3_step(stmt) ! SQLITE_DONE) { printf(insert error: %s\n, sqlite3_errmsg(db)); break; } } sqlite3_finalize(stmt);这里的关键点sqlite3_prepare_v2把 SQL 文本解析成语句对象后续绑定、执行都在这个对象上操作。绑定参数的索引从 1 开始对应 SQL 里的?。SQLITE_TRANSIENT告诉 SQLite3 在需要时自行拷贝传入的字符串内容避免我们在栈上定义的缓冲区被提前释放。sqlite3_reset和sqlite3_clear_bindings的调用顺序没我写的这么严格reset主要重置语句执行状态clear_bindings清绑定的值两者配合才能安全复用一条语句。3.3 事务从每秒几百条到每秒上万条SQLite3 单条插入本身不慢但如果你一条一条执行每次插入都涉及一次事务提交磁盘同步的开销会拖垮性能。我做过一次实测不显式开启事务插入 1 万条数据大概要 20 多秒用BEGIN和COMMIT包住整个插入循环同样数据量能压到 1 秒内。这个差距在 VC6 老程序里体感更明显因为老机器磁盘性能本来就弱。事务的写法非常简单用sqlite3_exec直接执行sqlite3_exec(db, BEGIN;, 0, 0, 0); /* 上面 3.2 的批量插入循环 */ sqlite3_exec(db, COMMIT;, 0, 0, 0);注意如果循环中途出错记得执行ROLLBACK;回滚事务否则连接关不掉数据库文件也一直处于锁定状态。我在实际编码中会用一个标志位记录是否出错最后统一判断 COMMIT 还是 ROLLBACK。3.4 查询用 step 一行一行取查询的套路和插入类似也是 prepare、bind、step、取列值、finalize 五步。每调用一次sqlite3_step结果集向前推进一行返回SQLITE_ROW就表示当前行有数据返回SQLITE_DONE表示结果集取完了。const char *sqlQuery SELECT id, name, age FROM t_user WHERE age ?;; sqlite3_stmt *qstmt NULL; int id; const unsigned char *name; int age; sqlite3_prepare_v2(db, sqlQuery, -1, qstmt, NULL); sqlite3_bind_int(qstmt, 1, 25); while (sqlite3_step(qstmt) SQLITE_ROW) { id sqlite3_column_int(qstmt, 0); name sqlite3_column_text(qstmt, 1); age sqlite3_column_int(qstmt, 2); printf(id%d name%s age%d\n, id, name, age); } sqlite3_finalize(qstmt);name是unsigned char*类型直接用%s打印没问题但如果你要把它转成CString或复制到别的缓冲区记得先强转成const char*再考虑编码转换问题。4. 我实际踩过的一组 VC6 SQLite3 运行坑4.1 中文路径与 UTF-8 编码最大的隐形炸弹SQLite3 的sqlite3_open接口只接受 UTF-8 编码的字符串而 VC6 里默认的char*是 ANSI 编码GBK。直接拿带中文的 ANSI 路径去打开数据库结果是打不开、建不出文件甚至可能静默打开了一个乱码路径对应的空库。排查时你会在目标目录里看到一个名字是乱码的 .db 文件懂的人一眼就明白不懂的人会以为是数据丢了。解决办法有两种避免在数据库路径中使用中文这最简单。确实需要中文路径就在调用sqlite3_open前把 ANSI 字符串用MultiByteToWideChar转成 UTF-16再调用WideCharToMultiByte转成 UTF-8最后把 UTF-8 字符串传给sqlite3_open。VC6 自带MultiByteToWideChar这个转换函数在 Windows 平台上很成熟代码如下char utf8Path[512]; WCHAR widePath[256]; MultiByteToWideChar(CP_ACP, 0, ansiPath, -1, widePath, 256); WideCharToMultiByte(CP_UTF8, 0, widePath, -1, utf8Path, 512, NULL, NULL); /* 然后 sqlite3_open(utf8Path, db); */同样的问题也出现在存入数据库的文本上。如果你的程序直接从界面拿到CStringANSI 编码写入 SQLite3 之前不转成 UTF-8读出来时也会是乱码。建议在项目里封装两个函数AnsiToUtf8和Utf8ToAnsi所有进出 SQLite3 的字符串都过一遍这两个函数就再也不会因为编码出问题。4.2 sqlite3_exec 回调函数闭包陷阱与野指针sqlite3_exec可以传一个回调函数每查询出一行数据就回调一次。VC6 时代的 C 程序员习惯在回调函数里把数据写到全局变量里这个做法简单但容易出问题如果同一个数据库连接在多线程或多次查询中复用全局变量的值会被后一次查询覆盖。更隐蔽的坑是回调函数内使用局部的结构体指针时如果这个结构体生命周期管理不善会出现野指针。我建议新写的代码统一走 prepare/step 路线完全不用回调。sqlite3_exec只用来执行没有返回结果的语句比如 CREATE TABLE、BEGIN、COMMIT查询一律 prepare。这样代码可读性更好也少一类内存崩溃的风险。4.3 LNK 链接错误多半是 lib 没对上接入 sqlite3.lib 后最常看到的链接错误是unresolved external symbol _sqlite3_open。原因基本就是三种忘了加#pragma comment(lib, sqlite3.lib)或者工程 Link 设置里没有指定库路径。VC6 的库搜索路径有两层Tools Options 里的全局路径和 Project Settings 里的工程路径漏掉任何一个都可能导致找不到 lib。sqlite3.h的版本和sqlite3.lib的版本不一致比如头文件是新版声明了sqlite3_open_v2但 lib 是老版本的符号表对不上。解决办法是把头文件和 lib 统一替换成同一份源码生成的文件。明明是 C 工程但sqlite3.h没有用extern C包裹。虽然 SQLite3 的头文件自己处理了 C 兼容性但如果你在包含头文件之前定义了某些影响头文件内容解释的宏仍可能破坏extern C的逻辑。稳妥做法是在主程序里这样写extern C { #include sqlite3.h }这样编译器保证以 C 符号表去链接和 l ib 里导出的符号一致。4.4 VC6 调试版运行库和释放版混用这是一个隐藏得比较深的坑。VC6 的 Debug 版默认链接msvcrtd.dll对应的调试运行库Release 版链接msvcrt.dll。如果你在主工程里用 Release 配置编译但 sqlite3.lib 是 Debug 配置产出的运行时会时不时崩溃在内存分配或free上而且崩溃位置完全随机。排查难度极高。我在项目里是规定 sqlite3.lib 必须和主工程同配置、同日构建打包交付前整体 Rebuild 一遍才能保证现场不出幺蛾子。5. PRAGMA 调优与内存数据库老机器上的上上策5.1 三个最有效的 PRAGMA 设置接入 SQLite3 只是第一步老工控机配置低、磁盘可能还是机械硬盘想让数据库跑得顺必须做几个基础调优。这几个 PRAGMA 在程序初始化时执行一次即可PRAGMA 命令作用适用建议PRAGMA synchronous NORMAL;降低磁盘同步频率减少写入等待需要数据安全性和速度平衡时用 NORMAL追求极致速度用 OFFPRAGMA cache_size 8000;设置页缓存大小为 8000 页约 32MB内存充足时提升读性能明显PRAGMA journal_mode WAL;启用预写日志模式读不阻塞写多线程读写场景下强烈推荐journal_modeWAL在老环境里要注意一个前提WAL 模式会在数据库同目录下创建-wal和-shm两个附属文件如果你现场的程序需要把整个目录打包拷贝必须在关闭数据库后把这三个文件一起拷贝。另外某些老网络映射磁盘对多文件支持不好WAL 模式可能报错遇到这类情况就退回默认的 delete 模式不要硬撑。PRAGMA synchronousNORMAL是性价比最高的一项设置。默认是 FULL每次事务提交都要把数据刷到物理磁盘机械硬盘上很明显能感到卡顿。改成 NORMAL 后在多数情况下不会损坏数据库只可能在操作系统突然断电的极端场景下丢失最近几次事务。对工控现场的数据采集来说这个风险通常可以接受但如果涉及财务或关键记录别开 OFF最多 NORMAL。5.2 内存数据库临时数据的绝佳去处SQLite3 支持把数据库完全放到内存中连接字符串传:memory:即可。这个特性我经常用来做上位机程序的临时数据容器比如设备上报的实时数据要先做一轮过滤、排序、去重再用最终结果更新界面。用数组或者自定义链表也能做但要写一堆排序查找逻辑开一个内存库SQL 一把梭代码量能缩减一半以上。sqlite3 *memdb NULL; sqlite3_open(:memory:, memdb); /* 建表、插入、查询都在内存里完成不产生任何磁盘文件 */内存库有个坑每次连接关闭后数据全部消失。如果需要在某个时刻把内存库内容持久化到磁盘可以用VACUUM INTO语法新版支持或手动查内存库数据再插入磁盘库。VC6 用的老版本可能不支持VACUUM INTO稳妥做法是逐表读出来再写进磁盘库。这个做法虽然土但可靠。5.3 别忘了索引很多从 SQLite 入门的人容易忽略索引。数据量小的时候全表扫描没感觉数据涨到几十万条一条WHERE age 25的查询可能要卡几百毫秒。建立索引很简单CREATE INDEX idx_user_age ON t_user(age);但索引不是越多越好每多一个索引插入和更新时就要额外维护一份 B 树结构写入性能会下降。我的原则是查询频繁的字段建索引写入为主的表严格控制索引数量。VC6 老工程的内存和 CPU 资源都紧张索引设计尤其要克制。6. 留给后来人的几个实在建议SQLite3 接入 VC6.0本质上是让一个九十年代的编译器和二十一世纪的嵌入式数据库共存。整个过程最有价值的经验概括起来就三条第一版本选型要对别贪新SQLite3 3.6.x/3.7.x 是 VC6 最舒服的区间第二静态库不要省部署时少一个 DLL 就少一个故障点第三编码问题要在一开始就处理好ANSI 与 UTF-8 的转换必须做成公共函数别等到现场数据乱了才开始补。我个人的实际操作习惯是在工程根目录放一个third_party\sqlite3\文件夹里面同时保存sqlite3.c、sqlite3.h、sqlite3.lib以及一份编译说明 TXT。这样哪怕过了两三年之后重装系统、重新拉工程也不用再上网找旧版本源码。SQLite3 的官网版本更新很快老版本链接随时可能失效本地留一份备份比什么都靠谱。如果你手上的老工程也需要本地存储能力大可以按这个路子放心去折腾这条路我已经替你趟过了。本文还有配套的精品资源点击获取
返回列表