如何从非 C++ 语言通过 LevelDB 的 C 接口操作数据库:稳定 ABI 约定与错误指针用法
【免费下载链接】leveldbLevelDB is a fast key-value storage library written at Google that provides an ordered mapping from string keys to string values.项目地址: https://gitcode.com/GitHub_Trending/leveldb4/leveldb
把 LevelDB 集成到非 C++ 项目时(Java 走 JNI,或任何能通过 FFI 调用共享库的语言),不要碰 C++ 接口,走 C 接口。LevelDB 在公开头文件 include/leveldb/c.h 中提供这套接口,头文件注释明确说明它可以作为“a stable ABI”,用于“programs that keep leveldb in a shared library, or for a JNI api”。你要完成的事是:把 LevelDB 构建成共享库,在目标语言里声明同一组 C 签名,再按头文件约定的指针、长度和错误字符串规则调用。本文以仓库自带的完整 C 程序 db/c_test.c 为参照实现走通全流程,最后用它作为验证程序。适用版本为 1.23.0(CMakeLists.txt 中project(leveldb VERSION 1.23.0)),构建要求 CMake 3.22 及以上。
C 接口为什么是稳定 ABI:五条约定与不支持项
c.h 中所有公开函数都声明在extern "C"块内,函数名不加 C++ 名称修饰直接导出,且每个函数都带LEVELDB_EXPORT标记。头文件注释给出五条约定,它们是任何调用语言做声明时的依据:
- 只暴露不透明结构体指针:客户端只能看到
leveldb_t*、leveldb_options_t*这类类型,看不到内部结构,因此内部表示可以变更而不需要重编译客户端。 - 没有 Slice 类型:key 和 value 以“指针 + 长度”两个独立参数传递,它们不是以 NUL 结尾的字符串——doc/index.md Slice 一节说明 key 和 value 允许包含
'\0'字节。 - 错误用 NUL 结尾的 C 字符串表示:
NULL表示无错误;所有可能出错的操作都把char** errptr作为最后一个参数。进入函数时*errptr必须是NULL或一个malloc()出的错误串(在 Windows 上必须是本库malloc()出来的);成功时*errptr保持不变,失败时旧值被释放并写入新的malloc()错误消息。 - 布尔量用
uint8_t:0 为 false,其余为 true。 - 指针参数必须非 NULL——但注意仓库自己的测试代码对个别可选参数传了 NULL(如
leveldb_compact_range(db, NULL, 0, NULL, 0)、leveldb_options_set_info_log(options, NULL)),写客户端时以 c_test.c 的实际调用为准。
头文件同时明确列出不支持的项目:
- 没有 option 类型的 getter(只有 setter);
- 不支持实现了 key shortening 的自定义 comparator——实现 db/c.cc 把
FindShortestSeparator/FindShortSuccessor处理成空操作; - 不能仅用 C 绑定提供自定义的 iter、db、env、cache 实现。
内存方面有一个专用函数leveldb_free(void* ptr):头文件注释说明它用于释放本库malloc()返回的缓冲区(get 的值、错误串、目录名、property 值等),并且在某些平台(通常是 Windows)必须调用它而不是free(ptr)。
构建 LevelDB 共享库
LevelDB 用 CMake 构建,默认产物是静态库。CMakeLists.txt 对BUILD_SHARED_LIBS有完整分支:开启后会加-fvisibility=hidden,只导出LEVELDB_EXPORT标记的符号,并定义LEVELDB_SHARED_LIBRARY宏,启用 include/leveldb/export.h 中的导入/导出逻辑(Windows 上是__declspec(dllexport/dllimport),其他平台为空宏)。这是“任何语言只能看到一组稳定导出符号”的前提:
mkdir -p build && cd build cmake -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release .. cmake --build .测试默认开启(LEVELDB_BUILD_TESTS为 ON),db/c_test.c会被注册为名为c_test的可执行测试程序,构建需要third_party/googletest子模块——按 README.md 的说明获取源码时保留--recurse-submodules。如果只想产出共享库、不构建测试,可加-DLEVELDB_BUILD_TESTS=OFF -DLEVELDB_BUILD_BENCHMARKS=OFF。
参照 c_test.c 写一个最小 C 客户端
db/c_test.c 是一个可直接运行的 C 程序:它只包含leveldb/c.h,依次走创建 options、销毁旧库、打开、put、get、迭代、关闭。从它的主路径中提取出的最小程序如下(目录名、key、value 均取自 c_test.c 与 doc/index.md 的原文):
#include "leveldb/c.h" #include <stdio.h> #include <stdlib.h> #include <string.h> static void Free(char** ptr) { if (*ptr != NULL) { leveldb_free(*ptr); *ptr = NULL; } } static void CheckNoError(const char* phase, char** err) { if (*err != NULL) { fprintf(stderr, "%s: %s\n", phase, *err); abort(); } } int main(void) { const char* dbname = "/tmp/testdb"; char* err = NULL; leveldb_options_t* options = leveldb_options_create(); leveldb_options_set_create_if_missing(options, 1); leveldb_writeoptions_t* woptions = leveldb_writeoptions_create(); leveldb_writeoptions_set_sync(woptions, 1); leveldb_readoptions_t* roptions = leveldb_readoptions_create(); leveldb_readoptions_set_verify_checksums(roptions, 1); /* c_test.c 的“创建新数据库”模式:先销毁目录里的旧数据,再 open。 副作用:该目录中已有的数据库会被整体销毁。 首次运行时目录里没有数据库,c_test.c 在此处也不检查 err,只释放它。 */ leveldb_destroy_db(options, dbname, &err); Free(&err); leveldb_t* db = leveldb_open(options, dbname, &err); CheckNoError("open", &err); Free(&err); leveldb_put(db, woptions, "foo", 3, "hello", 5, &err); CheckNoError("put", &err); Free(&err); size_t val_len; char* val = leveldb_get(db, roptions, "foo", 3, &val_len, &err); CheckNoError("get", &err); Free(&err); /* 返回值是 malloc() 出的字节数组,不以 NUL 结尾,长度来自 val_len */ if (val == NULL || val_len != 5 || memcmp(val, "hello", 5) != 0) { fprintf(stderr, "unexpected value from get\n"); abort(); } leveldb_free(val); leveldb_iterator_t* iter = leveldb_create_iterator(db, roptions); leveldb_iter_seek_to_first(iter); while (leveldb_iter_valid(iter)) { size_t klen; const char* k = leveldb_iter_key(iter, &klen); printf("key: %.*s\n", (int) klen, k); leveldb_iter_next(iter); } leveldb_iter_get_error(iter, &err); CheckNoError("iter", &err); Free(&err); leveldb_iter_destroy(iter); leveldb_close(db); leveldb_readoptions_destroy(roptions); leveldb_writeoptions_destroy(woptions); leveldb_options_destroy(options); return 0; }两个约定要在这里落实。其一,每个leveldb_*_create出来的对象都有对应的leveldb_*_destroy,c_test.c 的 "cleanup" 阶段给出了完整释放顺序:close 数据库 → 销毁三个 options 对象 → 用leveldb_free释放库返回的字符串 → 销毁 cache/comparator/env。其二,数据库名是文件系统上的一个目录而不是文件(doc/index.md Opening A Database 一节),所有数据都存在该目录里。
编译这个程序时,包含路径用仓库根目录下的include/(CMakeLists 为 leveldb 目标声明的公开包含目录),链接刚构建出的libleveldb。下面的<REPO_ROOT>、<BUILD_DIR>是占位符,替换为你自己的仓库根目录和构建目录:
cc -I<REPO_ROOT>/include my_client.c -o my_client -L<BUILD_DIR> -lleveldb若运行时找不到libleveldb,把动态库查找路径指向<BUILD_DIR>(POSIX 下可用LD_LIBRARY_PATH)。
错误指针 errptr 的三个真实用法
errptr是整个 C 接口唯一的错误通道。仓库给出了三种必须完整实现到目标语言里的用法:
- 检查并立即报告。c_test.c 的
CheckNoError宏:err != NULL时打印文件名、行号、当前阶段名和错误串,然后abort()。上面的最小程序按同样方式处理。 - 用后释放并复位,才能继续用。c_test.c 的
Free(&err)释放字符串并把err置回NULL,后续调用复用同一个err变量——这正是约定 3 中“进入时*errptr必须是NULL或 malloc 出的消息”的要求:用完必须释放回NULL。 - 失败时句柄本身就是 NULL。c_test.c 的 "open_error" 阶段展示:设置
leveldb_options_set_error_if_exists后再打开一个已存在的库,leveldb_open返回 NULL 且err非 NULL。所以判断是:db == NULL且err非 NULL 即打开失败。
leveldb_get的返回是三分支,不要混淆:err非 NULL 表示读操作失败;val为 NULL 且err为 NULL 表示键不存在(c.h 注释原文 “Returns NULL if not found”);val非 NULL 时才是值,长度在*vallen里。
leveldb_property_value不接收errptr:属性名未知时返回 NULL,有效属性(c_test.c 用"leveldb.stats")返回malloc()出的字符串,需用leveldb_free释放。c_test.c 的 "property" 阶段就以此为检查方式:"nosuchprop"返回 NULL,"leveldb.stats"返回非 NULL。
在非 C++ 语言中声明并调用
仓库没有提供其他语言的绑定示例,C API 的实现只有 include/leveldb/c.h、db/c.cc 和 db/c_test.c 三处,所有声明都要以 c.h 为准。映射规则是固定的:
- 函数:c.h 中每个函数对应目标语言里一条声明,签名逐字照抄;函数都在
extern "C"块内,导出符号名就是函数名。 - 不透明类型:
leveldb_t、leveldb_options_t等映射为任意指针/uintptr 类型,客户端只做传递,不访问内部。 - 字节数据:key、value 一律“指针 + 独立长度参数”;传给
leveldb_open的目录名和传给leveldb_property_value的属性名是普通 C 字符串。 - 函数指针参数:自定义 comparator、自定义 filterpolicy、
leveldb_writebatch_iterate的回调都是 C 函数指针;目标语言 FFI 若给不出 C 兼容的函数指针,这些 API 就用不了,而不依赖回调的 API(例如只收一个整数的leveldb_filterpolicy_create_bloom(int bits_per_key))仍然可用。 - 内存:库返回的缓冲区一律用
leveldb_free释放;Windows 上头文件明确要求不能用语言运行时自己的 free 代替。 - 版本检查:加载共享库后先调
leveldb_major_version()/leveldb_minor_version()。c_test.c 开头的断言是major >= 1且minor >= 1;当前版本号为 major 1、minor 23(include/leveldb/db.h 中kMajorVersion/kMinorVersion,CMake 项目版本 1.23.0)。
用仓库自带的 c_test 验证整条 C 路径
验证程序就是仓库自带的 c_test:CMakeLists.txt 通过leveldb_test("db/c_test.c")把它注册为可执行程序,构建后得到名为c_test的可执行文件。跑一次它,覆盖 C 接口的主链路:版本检查、error_if_exists 打开失败判定、create_if_missing 打开成功、put/get、compact_range、writebatch(含 clear/append/iterate)、正反向迭代、approximate_sizes(断言两个键区间的 size 均大于 0)、property、snapshot(删除foo后,经快照读仍得到hello,去掉快照后读到 NULL)、leveldb_repair_db修复流程、自定义 filter 与 bloom filter(10 bits per key)、最后是完整清理。
按程序源码行为判断输出:运行中逐阶段打印阶段名(格式=== Test <phase>),全部通过后打印PASS;任何CheckNoError/CheckCondition失败时会打印文件名、行号、当前阶段和错误/条件串,然后 abort。看不到PASS时,abort 行直接指出 C 路径在哪个阶段断掉。
限制与常见判断
- C 接口不支持 option getter、实现 key shortening 的 comparator,以及仅用 C 绑定提供自定义 iter/db/env/cache 实现(c.h 头注释原文列出不支持项)。
- 一个数据库同一时间只能被一个进程(可多线程)打开,且库本身没有 client-server 模式(README.md Limitations)——把共享库包成长驻服务时保持“一个进程一个库”。
- 怀疑数据库损坏时,可在打开前
leveldb_options_set_paranoid_checks,打开失败后用leveldb_repair_db尽量恢复数据(doc/index.md Checksums 一节;c_test.c 的 "repair" 阶段是完整示例:close 后把create_if_missing/error_if_exists置 0,repair 再重新 open 并复核读取结果)。 - 本文程序中的
leveldb_destroy_db会销毁目标目录中的数据库,只对它确定可以重建的目录调用。
c_test 打印PASS之后,下一步就是把最小程序的主路径换成业务自己的 key/value:errptr检查与
【免费下载链接】leveldbLevelDB is a fast key-value storage library written at Google that provides an ordered mapping from string keys to string values.项目地址: https://gitcode.com/GitHub_Trending/leveldb4/leveldb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考