今天遇到一个奇怪的现象,前后两个版本 Cargo.lock 相同,但是后面的版本却报错”dyld: Library not loaded: @rpath/libclang.dylib”。
复现
- 没有问题的版本(旧版本)
https://github.com/pingcap/tidb-engine-ext/commit/01454150386e05c978a8970613b2426354d0fd0a - 有问题的版本(新版本)
https://github.com/CalvinNeo/tidb-engine-ext/tree/demo/article-for-bindgen
具体报错如下
1 | Finished dev [unoptimized] target(s) in 2.51s |
首先,这两个版本之间的区别是啥呢?主要两点:
- 将 workspace 改成 virtual
也就是有 workspace 段,但是没有 package 段。 - 将 cargo dependency 从 path 改成 git
它们都不涉及 gen-proxy-ffi 这个 crate。
调查
深入了解情况
“dyld: Library not loaded” 这个错误表示 gen-proxy-ffi 依赖 libclang 这个库,也就是所谓的 clang-sys,并且要在运行期加载,但我们并没有在运行期找到这个库。我们可以通过类似 ldd 的指令来确认这一点。
1 | otool -L target/debug/gen_proxy_ffi |
从 https://github.com/rust-lang/rust-bindgen/tree/v0.57.0 可以看到,这个项目确实依赖 clang-sys
1 | [dependencies] |
所以我们的问题是,为啥旧版本能跑呢?
通过 unit-graph 分析
可以通过--unit-graph
命令来检查编译时实际的依赖
1 | cargo build -Z unstable-options --unit-graph --package gen-proxy-ffi --bin gen_proxy_ffi |
通过下面的代码可以找到所有的 bindgen。
1 | bindgens = [v for v in j['units'] if v['pkg_id'].startswith("bindgen")] |
对于旧版本,我们发现有两个 bindgen 的项目。其中一个 bindgen 依赖 index 为 4 和 11 的两个项目,而另一个则不依赖任何项目。
1 | {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']}} |
这里很多 extern_crate_name 都是 build_script_build,具体看不出究竟是什么。但可以通过 index 直接对应到 “units” 这个数组的下标。检查发现,其中包含了 clang-sys。
1 | {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']}} |
对于新版本,发现只有一个 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 |
|
于是恍然大悟,如果指定让程序懒加载 libclang,但实际上我们又不会真的去用到它,那样老版本代码确实可以这么很苟地运行。因此我们去掉了default-features = false
,结果运行正常了。