libSQL 源码详解:sqlite3-jni,在 Java 中一比一映射 SQLite3 C API
【免费下载链接】libsqllibSQL is a fork of SQLite that is both Open Source, and Open Contributions.项目地址: https://gitcode.com/GitHub_Trending/li/libsql
sqlite3-jni 是 libSQL 仓库内(libsql-sqlite3源码树)自带的一套 Java Native Interface(JNI)绑定,目标是把 sqlite3 的 C API 以「尽可能一比一」的方式原样暴露给 Java 开发者,让 SQLite 官方 C 文档几乎可以当作 JNI 版的文档直接阅读。本文将基于 ext/jni/README.md 并结合仓库内真实源码,完整讲解该绑定的设计目标、Hello World、构建流程、与 C API 的映射规则,以及 Collation 与 UDF 等回调型 API 在 Java 中的重映射方式,帮助你在自己的 Java 项目中直接复用这套 C 风格 API。
前置说明:本 README 明确给出 FOREWARNING——该子项目仍处于快速开发阶段,API 可能随时变化;随 3.43 版本发布的 JNI 绑定属于 "tech preview"(技术预览),在正式定稿之前不要依赖其 API 细节,定稿后将提供强向后兼容保证。阅读本文时请留意这一前提。
一、项目定位与设计目标
该目录存放的是 sqlite3 API 的 Java Native Interface 绑定。核心设计目标(README 原话)如下:
- C API 的一比一(近似)映射:在跨语言语义允许的范围内,将 C API 以 1-to-1(-ish) 的方式映射到 Java;紧邻目标是尽可能让 C 文档 直接适用于 JNI 绑定(该外部链接仅作为背景说明,不构成仓库证据)。
- 支持 Java 8(2014 年发布)及更高版本。
- 环境无关:凡是 Java 与 SQLite3 都能运行的地方,绑定都应能工作。
- 除 JDK 外零第三方依赖:包括不引入特定 IDE 与工具链的构建级依赖;欢迎为任意环境补充构建文件,前提是它们互不干扰、不成为 SQLite 维护者的负担。
同时 README 明确列出Non-goals(非目标):
- 不提供高层 OO 包装 API——客户端可以基于这套 C 风格 API 自行封装。
- 虚拟表(Virtual Table)大概率不支持,因为将其塞进 Java 需要海量胶水代码。
- 不支持混合模式(客户端既通过 Java 侧 API、又通过自有 native 代码访问 SQLite),因为这会是本绑定与混合模式客户端之间潜在误交互的雷区。
从源码结构看,绑定代码位于 ext/jni/src/org/sqlite/jni 下,分为capi(C 风格 API 的直接映射)、wrapper1(一个轻量 Java 风格包装层)、fts5(FTS5 定制扩展)与tester等子包;C 侧胶水代码在 ext/jni/src/c/sqlite3-jni.c 与机器生成的 ext/jni/src/c/sqlite3-jni.h 中。
二、Hello World:打开一个内存数据库
README 给出的最小可运行示例完整如下:
import org.sqlite.jni.*; import static org.sqlite.jni.capi.CApi.*; ... final sqlite3 db = sqlite3_open(":memory:"); try { final int rc = sqlite3_errcode(db); if( 0 != rc ){ if( null != db ){ System.out.print("Error opening db: "+sqlite3_errmsg(db)); }else{ System.out.print("Error opening db: rc="+rc); } ... handle error ... } // ... else use the db ... }finally{ // ALWAYS close databases using sqlite3_close() or sqlite3_close_v2() // when done with them. All of their active statement handles must // first have been passed to sqlite3_finalize(). sqlite3_close_v2(db); }几个值得注意的实现细节,均可在仓库源码中得到印证:
sqlite3是一个「指针包装器」而非连接对象。查看 sqlite3.java:它继承自NativePointerHolder<sqlite3>并实现AutoCloseable,其 Javadoc 明确说明「这些包装器并不拥有其关联的指针,只是通过 JNI 在 Java 与 C 之间以类型安全的方式传递它」。它的close()方法内部直接调用CApi.sqlite3_close_v2(this)。CApi是全部绑定的唯一入口。查看 CApi.java 第 86-90 行:CApi是final类,其静态初始化块执行System.loadLibrary("sqlite3-jni")——这就是为什么运行时必须保证libsqlite3-jni.so(或其他平台对应的 DLL)能被 JVM 通过-Djava.library.path=...找到。- 结果码语义与 C 完全一致:
sqlite3_open、sqlite3_errcode、sqlite3_errmsg等函数的返回码与 C API 一一对应(SQLITE_OK为 0,等等),这些常量全部以long形式声明在机器生成的sqlite3-jni.h中,例如SQLITE_OK 0L、SQLITE_ERROR 1L、SQLITE_BUSY 5L、SQLITE_ROW 100L、SQLITE_DONE 101L。
三、构建与测试
README 指出,标准构建假定类 Linux 环境,需要:
- GNU Make
- 支持 Java 8 或更高版本的 JDK
- 现代 C 编译器(gcc 和 clang 均可)
最简单的构建流程:
$ export JAVA_HOME=/path/to/jdk/root $ make $ make test $ make cleanmake jar可生成 jar 发行包,但jar 不包含二进制 DLL 文件——每个目标平台都需要单独编译各自的 DLL。
仓库中的 GNUmakefile 为我们揭示了远比 README 更丰富的构建细节:
- JDK 探测:
JAVA_HOME ?= $(HOME)/jdk/current,JDK_HOME ?= $(JAVA_HOME);若$(JDK_HOME)不存在会直接报错set JDK_HOME to the top-most dir of your JDK installation.。jar/java/javac/javadoc 全部取自$(JDK_HOME)/bin/。 - JNI 头文件路径:编译 C 侧时除
$(JDK_HOME)/include外,还通过$(patsubst ...)自动把$(JDK_HOME)/include/*下的平台子目录(如linux)加入头文件搜索路径(-I)。 - 可调编译开关:
opt.threadsafe(默认 1)、opt.fatal-oom(默认 1)、opt.debug(默认 1,开启-DSQLITE_DEBUG -g;关闭时用-Os)、opt.metrics(默认 1,对应-DSQLITE_JNI_ENABLE_METRICS)。基础宏包括-DSQLITE_THREADSAFE=...、-DSQLITE_TEMP_STORE=2、-DSQLITE_USE_URI=1、-DSQLITE_OMIT_LOAD_EXTENSION、-DSQLITE_OMIT_DEPRECATED、-DSQLITE_OMIT_SHARED_CACHE。 - 可选扩展宏:
opt.extras=1时追加-DSQLITE_ENABLE_RTREE、-DSQLITE_ENABLE_PREUPDATE_HOOK、-DSQLITE_ENABLE_COLUMN_METADATA、-DSQLITE_ENABLE_EXPLAIN_COMMENTS、-DSQLITE_ENABLE_NORMALIZE、-DSQLITE_ENABLE_SQLLOG、-DSQLITE_ENABLE_STMTVTAB及 dbpage/dbstat/bytecode vtab 等;enable.fts5=1时追加-DSQLITE_ENABLE_FTS5并把 fts5 相关 Java 文件纳入编译(GNUmakefile 注释特别提醒:该开关会影响已签入的sqlite3-jni.h内容,剥离 fts5 的改动不应被签入)。 - JNI 头文件的生成机制:
sqlite3-jni.h是机器生成的,由javac -h针对含 JNI 声明的 Java 文件(CApi.java、SQLTester.java,以及启用 fts5 时的Fts5ExtensionApi.java、fts5_api.java、fts5_tokenizer.java)生成各org_sqlite_jni_*.h,再拼接为最终头文件。 - 测试目标:
make test实际执行test-one与test-mt——test-one运行org.sqlite.jni.capi.Tester1和org.sqlite.jni.wrapper1.Tester2;test-mt以-t 7 -r 50 -shuffle参数做 7 线程 × 50 轮的随机顺序多线程测试。另有test-sqllog(带-sqllog选项)与tester(驱动src/tests/*.test脚本)。JVM 参数统一带-ea(启用断言)与-Djava.library.path=$(dir.bld.c)。 - 多线程模式矩阵测试:
make multitest会依次用threadsafe=0/1/2×oom=0/1共 6 种组合编译并跑完整测试集。 - jar 与发行包:
make jar生成sqlite3-jni.jar,主类为org.sqlite.jni.capi.Tester1,并提示运行需要-Djava.library.path=DIR/CONTAINING/libsqlite3-jni.so;make dist生成发行 zip(sqlite-jni-<版本>.zip),且会警告必须使用 JDK8(javac 1.8)构建以保证兼容性。 - javadoc:
make doc生成 javadoc,默认-exclude org.sqlite.jni.fts5(2023-09-13 起暂不把 fts5 部分纳入公开文档)。
四、C API 的一比一映射规则
4.1 映射哲学
README 的核心原则:本绑定力求在跨语言语义允许的范围内,提供与 C API 文档一致的一比一体验。凡是跨语言语义不允许一比一的地方,或一比一映射在 Java 中会显得异常笨拙的地方,才会审慎地调整接口;所有偏离 C 语义的场合都会明确记录。任何语义偏差(无论是增加还是删减)都被要求清楚写进文档。
Java 侧重载(overloads):出于易用性,绑定会提供接受/返回替代数据形式或提供默认参数值的重载,但它们全部是对应 C API 的「薄代理」,不引入任何新语义。从 CApi.java 的源码注释可以看到,例如sqlite3_result_set(sqlite3_context, int)等价于sqlite3_result_int,且sqlite3_result_set()有大量按类型区分的重载。
Java 专属能力(_java后缀):少量新增 API 的名字里都带_java,README 给出的示例包括:
sqlite3_result_java_object()sqlite3_column_java_object()sqlite3_value_java_object()
三者合起来,实现「把任意 Java 对象从用户自定义 SQL 函数一路传递回调用方」。在 sqlite3-jni.h 中还能看到同类扩展,例如sqlite3_bind_java_object(Signature: (JILjava/lang/Object;)I)、sqlite3_bind_nio_buffer(把java.nio.ByteBuffer直接绑定为 BLOB)、sqlite3_jni_supports_nio()、sqlite3_java_uncache_thread()等。
Java 的 Modified UTF-8 陷阱:CApi.java的类注释专门提醒——SQLite 内部使用标准 UTF-8,而 Java 原生使用 UTF-16,JNI 的字符串转换用的是「Modified UTF-8(MUTF-8)」。因此 Java 字符串到标准 UTF-8 的转换必须走String.getBytes(StandardCharsets.UTF_8);接受 C 风格无长度字符串的函数,需要自行把 Java 字符串转成以\0结尾的字节数组(CApi内用nulTerminateUtf8()私有方法统一处理),而带长度参数的函数只要传对长度即可。这点在跨语言边界上极易踩坑,值得所有使用者警惕。
4.2 Golden Rule:垃圾回收无法释放 SQLite 资源
这是本绑定最重要的使用纪律,README 原文强调:
- 所有数据库与预处理语句句柄都必须由客户端代码显式清理;
- 有未释放语句句柄的数据库无法关闭。
sqlite3_close()在无法关闭时会失败;sqlite3_close_v2()则识别这种情况,把数据库标记为 "zombie"(僵尸),待库检测到所有挂起语句都关闭后再执行终结。 - Java 垃圾回收不能关闭数据库,也不能终结预处理语句——这些必须显式调用 API。
- 合适的类实现了 Java 的
AutoCloseable接口,因此可以配合 try-with-resources 使用(如上面的sqlite3类所示,其close()即sqlite3_close_v2)。
另外在 CApi.java 中还有一条与资源/线程相关的规则:每个使用过 SQLite3 JNI API 的线程,在结束前都应调用sqlite3_java_uncache_thread()来清理缓存的每线程信息;主线程不强制,但额外线程不调用会泄漏 C 堆上的缓存条目,进而可能持有大量 Java 侧全局引用。
4.3 Golden Rule #2:回调中严禁抛异常(除非……)
除明确文档化的例外,本 API 的所有例程都保留 C 风格语义:不允许抛出或传播异常,错误信息必须通过结果码或null返回。允许抛出的唯一例外是客户端误用(例如在会导致NullPointerException的地方传null)。API 会标记不应为 null 的参数,但一般不主动防御此类误用;部分 C 风格 API 明确把null当作 no-op,部分 JNI API 在收到null时会刻意返回错误码而非段错误。
客户端定义的回调绝对不允许抛异常,除非被非常明确地文档化为 throw-safe。原因在于(README 原文):所有此类回调都充当 C 函数回调接口的代理,而其中一些接口没有错误上报机制——有能力把错误传播回库的回调会把异常转换成对应的 C 级错误信息;没有传播能力的回调为了维持 C 风格语义,必然要抑制异常(可能只在调试通道输出)。
在源码中可以找到对应佐证:AggregateFunction.xStep()的 Javadoc 写的是「如果此函数抛出异常,异常不会被传播,可能向调试通道发出警告」;AggregateFunction.xFinal()与ScalarFunction.xFunc()则是「异常会被转换为sqlite3_result_error()」——前者没有错误传播通道,后者有,处理策略截然不同。
五、笨拙结构的重映射:Collation 与 UDF
有些 C 构造一比一搬进 Java 会非常别扭,因为它们把 C 的世界观强塞进 Java 迥异的模型里。README 用「我们先试过一比一,真的很别扭」的口吻,详解了以下几类重映射。
5.1 自定义排序规则(Custom Collations)
C API 原形(README 原文):
// C: int sqlite3_create_collation(sqlite3 * db, const char * name, int eTextRep, void *pUserData, int (*xCompare)(void*,int,void const *,int,void const *)); int sqlite3_create_collation_v2(sqlite3 * db, const char * name, int eTextRep, void *pUserData, int (*xCompare)(void*,int,void const *,int,void const *), void (*xDestroy)(void*));C 中pUserData是可选客户端状态,通过「外部传参」的方式传给xCompare()/xDestroy()。如果照搬到 Java,就会出现 README 所说的两处别扭:(A) 回调与状态是两个截然不同的对象;(B) 状态要单独提供,这与 Java 的习语格格不入。因此绑定改为如下 Java 接口(README 原文):
int sqlite3_create_collation(sqlite3 db, String name, int eTextRep, SomeCallbackType collation);其中Collation类提供必须实现的抽象call()方法与可覆写的 no-opxDestroy()方法,使用起来非常「Java」:
int rc = sqlite3_create_collation(db, "mycollation", SQLITE_UTF8, new SomeCallbackType(){ // Required comparison function: @Override public int call(byte[] lhs, byte[] rhs){ ... } // Optional finalizer function: @Override public void xDestroy(){ ... } // Optional local state: private String localState1 = "This is local state. There are many like it, but this one is mine."; private MyStateType localState2 = new MyStateType(); ... });仓库中的 CollationCallback.java 印证了这一设计:它继承CallbackProxy与XDestroyCallback,核心方法是int call(@NotNull byte[] lhs, @NotNull byte[] rhs)(要求按memcmp()语义返回比较结果)与void xDestroy()(排序规则被销毁时调用,可覆写以做自定义清理)。
README 还特别说明了几点:
- 如果需要,完全可以把状态通过闭包绑定在调用作用域内,而不是塞进 Collation 对象;
- 上述改造不丢失、不遮蔽 C API 的任何能力,资深用户无需妥协;
- 由于新接口同时提供了 v1 与 v2 的能力,
sqlite3_create_collation_v2()变得多余——覆写xDestroy()即等价于 v2 语义。
5.2 用户自定义 SQL 函数(UDF)
sqlite3_create_function()一族 API 重度依赖函数指针来提供客户端回调,因此 JNI 绑定必须改接口。Java 侧只有一个核心函数注册入口(README 原文):
int sqlite3_create_function(sqlite3 db, String funcName, int nArgs, int encoding, SQLFunction func);README 中的开放设计问题:
encoding参数在 Java 中是否还有意义?目前未定,若无意义将被移除。
SQLFunction本身不直接使用,而是通过其三个子类实例化(README 原文):
ScalarFunction:用单个回调实现简单标量函数;AggregateFunction:用两个回调实现聚合函数;WindowFunction:用四个回调实现窗口函数。
在仓库源码中可以看到这三者的精确形状:
- SQLFunction.java 是一个空标记接口(marker interface),注释说明 JNI 层只依赖基类
SQLFunction及 UDF 回调接口的方法名与签名,客户端甚至可以自建符合公共接口的类。 - ScalarFunction.java:抽象方法
xFunc(sqlite3_context cx, sqlite3_value[] args)(对应 C 的xFunc,抛异常会被转换为sqlite3_result_error())与可覆写的空xDestroy()。 - AggregateFunction.java:抽象方法
xStep(sqlite3_context cx, sqlite3_value[] args)与xFinal(sqlite3_context cx),并内嵌PerContextState<T>辅助类——它用Map<Long, ValueHolder<T>>把sqlite3_context.getAggregateContext()映射到一块客户端自定义类型的「累加器状态」,解决同一 SQL 语句中多次调用 UDF(如SELECT MYFUNC(A), MYFUNC(B)...)时状态归属的难题;配套的getAggregateState()(首次调用时以初值创建映射)与takeAggregateState()(在xFinal中取出并清除映射)必须配对使用。 - WindowFunction.java:继承
AggregateFunction<T>,额外要求实现xInverse(sqlite3_context cx, sqlite3_value[] args)与xValue(sqlite3_context cx)(对应 C APIsqlite3_create_window_function()的回调),从而凑齐 README 所说的「四个回调」。
README 建议在 Tester1.java 中搜索SQLFunction查看实际用法。该文件本身是一个约 2200 行的反射驱动测试集,支持单线程、多线程(-t、-r、-shuffle)模式,并用@ManualTest、@SingleThreadOnly、@RequiresJniNio等注解标记测试的执行约束。
5.3 诸如此类(And so on...)
README 最后指出,其他接受回调的 API 也采用与上面类似的接口改造,例如sqlite3_trace_v2()与sqlite3_update_hook()。尽管签名变了,JNI 层仍会尽一切努力提供与 C API 文档一致的语义。
从源码目录 capi 中可以数出这些回调接口的完整清单:AuthorizerCallback(授权器)、AutoExtensionCallback(自动扩展)、BusyHandlerCallback(忙处理器)、CollationNeededCallback、CommitHookCallback(提交钩子)、ConfigLogCallback、ConfigSqlLogCallback、PreupdateHookCallback(preupdate 钩子,对应-DSQLITE_ENABLE_PREUPDATE_HOOK)、ProgressHandlerCallback(进度处理器)、RollbackHookCallback(回滚钩子)、TraceV2Callback(sqlite3_trace_v2)、UpdateHookCallback(更新钩子)、PrepareMultiCallback、XDestroyCallback等,另有OutputPointer(模拟 C 的指针输出参数,例如sqlite3_open的sqlite3**第二参数)、ValueHolder(可修改的值容器)与NativePointerHolder(原生指针的 Java 包装基类)。
六、如何继续深入
- 阅读原文档:ext/jni/README.md(注意其开头的免责声明——API 仍在快速演进)。
- 看 C 侧胶水实现:sqlite3-jni.c 与机器生成的 sqlite3-jni.h(后者含全部结果码、
SQLITE_*常量与JNIEXPORT函数签名)。 - 看 Java 侧 API 全集:CApi.java(
final类,静态块System.loadLibrary("sqlite3-jni"))。 - 看句柄包装与资源管理:sqlite3.java、
sqlite3_stmt.java、sqlite3_value.java、sqlite3_context.java、sqlite3_blob.java、sqlite3_backup.java(均继承NativePointerHolder并支持AutoCloseable)。 - 看测试用例:Tester1.java(C 风格 API 测试)、Tester2.java(wrapper1 包装层测试)、TesterFts5.java(FTS5 测试),以及脚本驱动测试 src/tests。
- 看构建细节:GNUmakefile 与发行构建文件 jar-dist.make。
- 整个绑定是 libSQL(SQLite 的开源 fork)源码树的一部分,SQLite 核心位于 libsql-sqlite3/src,上层 Rust 生态(
libsql、libsql-server、libsql-ffi等)可参见仓库根 README.md。
【免费下载链接】libsqllibSQL is a fork of SQLite that is both Open Source, and Open Contributions.项目地址: https://gitcode.com/GitHub_Trending/li/libsql
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考