Z3求解器是一个微软开源的定理证明器,高效好用。有一天我突发奇想,想将其集成进typst中,因为理论上来说定理证明器涉及不到其他环境的交互,但我想的太简单了。最终我放弃了,写此文章记录探索的过程。
把z3集成进typst最简单的方法:就是编译出纯WASM文件,WASM作为插件。然后用typ文件包装好这个WASM文件,暴露出优雅的接口。
最开始我的想法是直接写Rust代码调用Z3,但发现Z3-sys的库本质上是调用的编译好的共享库,遂放弃纯Rust方法。
我开始阅读Z3的文档,然而并没有直接编译成WASM的教程,但却有提供JavaScript的NPM包,也有WASM的bindings。我顺藤摸瓜找到了src/api/js/scripts/build-wasm.ts
这么一个文件,内容如下:
// ...
// TODO(ritave): Detect if it's in the configuration we need
if (!existsSync(path.join(z3RootDir, 'build/Makefile'))) {
spawnSync('emconfigure python scripts/mk_make.py --staticlib --single-threaded --arm64=false', {
: z3RootDir,
cwd;
})
}
spawnSync(`emmake make -j${os.cpus().length} libz3.a`, { cwd: path.join(z3RootDir, 'build') });
// ...
从这些代码我可以看到首先使用emconfigure配置编译环境,然后用emmake编译出静态库。这些代码刚好给了我启发——我可以先编译出适配的静态库(因为Z3也提供了C语言接口),然后用Rust FFI 调用必要的C接口,结合 这个项目 方便的包装,编译出适配typst的wasm插件。 理想是美好的,现实是骨感的。
第一次尝试
于是,我先按照上面的ts代码,配置好3.1.15版本的emsdk(复现编译器的版本),编译出libz3.a 的静态库。
emconfigure python scripts/mk_make.py --staticlib --single-threaded --arm64=false
cd build
emmake make -j$(nproc) libz3.a
就这样,一个z3的静态库出炉了,当我欣喜若狂的写好Rust的适配代码调用静态库、链接最后编译出wasm文件、用typst调用wasm的时候,意外发生了——环境不支持SIMD指令。
原来是typst使用的wasmi库不支持SIMD指令。既然这样,那我就将SIMD的选项去掉。
第二次尝试
因为z3的配置选项并不支持直接屏蔽SIMD选项, 我只能直接对config.mk文件动手,修改后内容如下
CXXFLAGS= -D_MP_INTERNAL -DNDEBUG -D_EXTERNAL_RELEASE -std=c++17 -fvisibility=hidden -fvisibility-inlines-hidden -c -DSINGLE_THREAD -O3 -mno-simd128 -fno-tree-vectorize -fPIC -flto
CFLAGS= -D_MP_INTERNAL -DNDEBUG -D_EXTERNAL_RELEASE -fvisibility=hidden -fvisibility-inlines-hidden -c -DSINGLE_THREAD -O3 -mno-simd128 -fno-tree-vectorize -fPIC -flto
为了避免O3优化添加SIMD指令,在其后面添加-mno-simd128
和-fno-tree-vectorize
以阻止编译器生成带SIMD指令的WASM。
之后步骤如法炮制。结果运行时编译出来的库的有些符号未定义,查询资料得知有wasi的标准允许wasm和环境互动。emsdk编译出来的环境属于标准的wasi环境,需要对某些函数stub。
第三次尝试到放弃
我按照指令对关键函数stub,结果还是出现了符号未定义的问题。我将有些符号提取出来自己做了符号的定义。
#[no_mangle]
pub extern "C" fn __call_sighandler(a:i32,b:i32){
}
#[no_mangle]
pub extern "C" fn _emscripten_date_now() -> f64 {
1.0
}
#[no_mangle]
pub extern "C" fn _emscripten_get_now_is_monotonic() -> i32 {
// Assume the time source is monotonic
1
}
__call_sighandler
是处理信号用的,为了方便留下了空函数,_emscripten_date_now
和_emscripten_get_now_is_monotonic
则是emscripten环境下获取时间的函数。我猜测z3获取时间是为了不让计算的时间超过timeout,为了简化起见我设置成了时间是固定的,永远都是单调(递增)的。
原本以为定义了函数,又stub了不需要的函数就万事大吉了,结果最后老天给我开了一个玩笑。
error: plugin panicked: wasm `unreachable` instruction executed
┌─ test.typ:14:11
│
14 │ let ctx = z3.mk_context(cfg)
│ ^^^^^^^^^^^^^^^^^^
事已至此,我认为如果要完全将z3移植到typst里,就需要深入z3的代码之中,改动可能很大,而且也不一定能成功,遂放弃。