解决 bindgen 的一个奇怪问题

今天遇到一个奇怪的现象,前后两个版本 Cargo.lock 相同,但是后面的版本却报错”dyld: Library not loaded: @rpath/libclang.dylib”。

复现

  1. 没有问题的版本(旧版本)
    https://github.com/pingcap/tidb-engine-ext/commit/01454150386e05c978a8970613b2426354d0fd0a
  2. 有问题的版本(新版本)
    https://github.com/CalvinNeo/tidb-engine-ext/tree/demo/article-for-bindgen

具体报错如下

1
2
3
4
5
6
Finished dev [unoptimized] target(s) in 2.51s
Running `target/debug/gen_proxy_ffi`
dyld: Library not loaded: @rpath/libclang.dylib
Referenced from: /Users/calvin/tidb-engine-ext/target/debug/gen_proxy_ffi
Reason: image not found
zsh: abort cargo run --package gen-proxy-ffi --bin gen_proxy_ffi

首先,这两个版本之间的区别是啥呢?主要两点:

  1. 将 workspace 改成 virtual
  2. 将 cargo dependency 从 path 改成 git

它们都不涉及 gen-proxy-ffi 这个 crate。

调查

深入了解情况

“dyld: Library not loaded” 这个错误表示 gen-proxy-ffi 依赖 libclang 这个库,也就是所谓的 clang-sys,并且要在运行期加载,但我们并没有在运行期找到这个库。我们可以通过类似 ldd 的指令来确认这一点。

1
2
3
4
5
6
otool -L target/debug/gen_proxy_ffi
target/debug/gen_proxy_ffi:
@rpath/libclang.dylib (compatibility version 1.0.0, current version 1205.0.22)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1292.100.5)
/usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)
/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0)

https://github.com/rust-lang/rust-bindgen/tree/v0.57.0 可以看到,这个项目确实依赖 clang-sys

1
2
[dependencies]
clang-sys = { version = "1", features = ["clang_6_0"] }

所以我们的问题是,为啥旧版本能跑呢?

通过 unit-graph 分析

可以通过--unit-graph命令来检查编译时实际的依赖

1
cargo build -Z unstable-options --unit-graph --package gen-proxy-ffi --bin gen_proxy_ffi

通过下面的代码可以找到所有的 bindgen。

1
2
3
4
bindgens = [v for v in j['units'] if v['pkg_id'].startswith("bindgen")]
for x in bindgens:
print(x)
print("\n")

对于旧版本,我们发现有两个 bindgen 的项目。其中一个 bindgen 依赖 index 为 4 和 11 的两个项目,而另一个则不依赖任何项目。

1
2
3
4
{u'profile': {u'name': u'dev', u'codegen_units': None, u'debug_assertions': False, u'debuginfo': 0, u'codegen_backend': None, u'rpath': False, u'overflow_checks': False, u'incremental': False, u'strip': u'none', u'opt_level': u'0', u'split_debuginfo': None, u'lto': u'false', u'panic': u'unwind'}, u'features': [], u'platform': None, u'dependencies': [{u'index': 4, u'noprelude': False, u'public': False, u'extern_crate_name': u'build_script_build'}, {u'index': 11, u'noprelude': False, u'public': False, u'extern_crate_name': u'build_script_build'}], u'mode': u'run-custom-build', u'pkg_id': u'bindgen 0.57.0 (registry+https://github.com/rust-lang/crates.io-index)', u'target': {u'kind': [u'custom-build'], u'name': u'build-script-build', u'doc': False, u'src_path': u'/Users/calvin/.cargo/registry/src/github.com-1ecc6299db9ec823/bindgen-0.57.0/build.rs', u'edition': u'2018', u'doctest': False, u'test': False, u'crate_types': [u'bin']}}


{u'profile': {u'name': u'dev', u'codegen_units': None, u'debug_assertions': True, u'debuginfo': 0, u'codegen_backend': None, u'rpath': False, u'overflow_checks': False, u'incremental': False, u'strip': u'none', u'opt_level': u'0', u'split_debuginfo': None, u'lto': u'false', u'panic': u'unwind'}, u'features': [], u'platform': None, u'dependencies': [], u'mode': u'build', u'pkg_id': u'bindgen 0.57.0 (registry+https://github.com/rust-lang/crates.io-index)', u'target': {u'kind': [u'custom-build'], u'name': u'build-script-build', u'doc': False, u'src_path': u'/Users/calvin/.cargo/registry/src/github.com-1ecc6299db9ec823/bindgen-0.57.0/build.rs', u'edition': u'2018', u'doctest': False, u'test': False, u'crate_types': [u'bin']}}

这里很多 extern_crate_name 都是 build_script_build,具体看不出究竟是什么。但可以通过 index 直接对应到 “units” 这个数组的下标。检查发现,其中包含了 clang-sys。

1
2
{u'profile': {u'name': u'dev', u'codegen_units': None, u'debug_assertions': True, u'debuginfo': 0, u'codegen_backend': None, u'rpath': False, u'overflow_checks': False, u'incremental': False, u'strip': u'none', u'opt_level': u'0', u'split_debuginfo': None, u'lto': u'false', u'panic': u'unwind'}, u'features': [], u'platform': None, u'dependencies': [], u'mode': u'build', u'pkg_id': u'bindgen 0.57.0 (registry+https://github.com/rust-lang/crates.io-index)', u'target': {u'kind': [u'custom-build'], u'name': u'build-script-build', u'doc': False, u'src_path': u'/Users/calvin/.cargo/registry/src/github.com-1ecc6299db9ec823/bindgen-0.57.0/build.rs', u'edition': u'2018', u'doctest': False, u'test': False, u'crate_types': [u'bin']}}
{u'profile': {u'name': u'dev', u'codegen_units': None, u'debug_assertions': False, u'debuginfo': 0, u'codegen_backend': None, u'rpath': False, u'overflow_checks': False, u'incremental': False, u'strip': u'none', u'opt_level': u'0', u'split_debuginfo': None, u'lto': u'false', u'panic': u'unwind'}, u'features': [u'clang_3_5', u'clang_3_6', u'clang_3_7', u'clang_3_8', u'clang_3_9', u'clang_4_0', u'clang_5_0', u'clang_6_0'], u'platform': None, u'dependencies': [{u'index': 12, u'noprelude': False, u'public': False, u'extern_crate_name': u'build_script_build'}], u'mode': u'run-custom-build', u'pkg_id': u'clang-sys 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)', u'target': {u'kind': [u'custom-build'], u'name': u'build-script-build', u'doc': False, u'src_path': u'/Users/calvin/.cargo/registry/src/github.com-1ecc6299db9ec823/clang-sys-1.1.1/build.rs', u'edition': u'2015', u'doctest': False, u'test': False, u'crate_types': [u'bin']}}

对于新版本,我们发现只有一个 bindgen 项目,可以看到它会依赖 clang_sys。

1
{u'profile': {u'name': u'dev', u'codegen_units': 4, u'debug_assertions': True, u'debuginfo': 0, u'codegen_backend': None, u'rpath': False, u'overflow_checks': False, u'incremental': False, u'strip': u'none', u'opt_level': u'0', u'split_debuginfo': None, u'lto': u'false', u'panic': u'unwind'}, u'features': [], u'platform': None, u'dependencies': [{u'index': 3, u'noprelude': False, u'public': False, u'extern_crate_name': u'build_script_build'}, {u'index': 5, u'noprelude': False, u'public': False, u'extern_crate_name': u'bitflags'}, {u'index': 9, u'noprelude': False, u'public': False, u'extern_crate_name': u'cexpr'}, {u'index': 10, u'noprelude': False, u'public': False, u'extern_crate_name': u'clang_sys'}, {u'index': 18, u'noprelude': False, u'public': False, u'extern_crate_name': u'lazy_static'}, {u'index': 19, u'noprelude': False, u'public': False, u'extern_crate_name': u'lazycell'}, {u'index': 29, u'noprelude': False, u'public': False, u'extern_crate_name': u'peeking_take_while'}, {u'index': 30, u'noprelude': False, u'public': False, u'extern_crate_name': u'proc_macro2'}, {u'index': 33, u'noprelude': False, u'public': False, u'extern_crate_name': u'quote'}, {u'index': 34, u'noprelude': False, u'public': False, u'extern_crate_name': u'regex'}, {u'index': 36, u'noprelude': False, u'public': False, u'extern_crate_name': u'rustc_hash'}, {u'index': 38, u'noprelude': False, u'public': False, u'extern_crate_name': u'shlex'}], u'mode': u'build', u'pkg_id': u'bindgen 0.57.0 (registry+https://github.com/rust-lang/crates.io-index)', u'target': {u'kind': [u'lib'], u'name': u'bindgen', u'doc': True, u'src_path': u'/Users/calvin/.cargo/registry/src/github.com-1ecc6299db9ec823/bindgen-0.57.0/src/lib.rs', u'edition': u'2018', u'doctest': True, u'test': True, u'crate_types': [u'lib']}}

至此可以得出猜测,老版本之所以能运行原因是 gen-proxy-ffi 链接到了不依赖 clang-sys 的 bindgen 上了。

最终结论

可为什么依赖 clang-sys 的 bindgen 可以实际上不依赖 clang-sys 呢?为此,我们首先怀疑 build.rs,可并没有发现什么异常。

然后我们就尝试从源码中搜索 clang 关键词,然后我们发现了下面的代码。似乎我们可以在代码运行过程中,用类似 dlopen 的方式懒加载 libclang。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[cfg(feature = "runtime")]
fn ensure_libclang_is_loaded() {
if clang_sys::is_loaded() {
return;
}

// XXX (issue #350): Ensure that our dynamically loaded `libclang`
// doesn't get dropped prematurely, nor is loaded multiple times
// across different threads.

lazy_static! {
static ref LIBCLANG: std::sync::Arc<clang_sys::SharedLibrary> = {
clang_sys::load().expect("Unable to find libclang");
clang_sys::get_library().expect(
"We just loaded libclang and it had better still be \
here!",
)
};
}

clang_sys::set_library(Some(LIBCLANG.clone()));
}

于是恍然大悟,如果指定让程序懒加载 libclang,但实际上我们又不会真的去用到它,那样老版本代码确实可以这么很苟地运行。因此我们去掉了default-features = false,结果运行正常了。