01-互操作性
6 分钟阅读
译文 · 基于 Pragmatic Rust Guidelines
原文链接: https://microsoft.github.io/rust-guidelines/guidelines/libs/interop/index.html
互操作性
类型是 Send (M-TYPES-SEND)
本条守护:可在 Tokio 中使用,以及置于运行时抽象之后。
公开类型出于兼容性应当是 Send:
- 所产生的全部 future(无论显式还是隐式)必须是
Send - 其他大多数类型也应当是
Send,但可能有例外
Future
当显式声明 future 时,你应当确保它是、并且一直保持为 Send。
| |
当通过 async 方法调用隐式返回 future 时,你也应当确保它们是 Send。
不必测试每一个方法,但至少应验证主要入口点。
| |
普通类型
大多数普通类型应当是 Send,否则若跨 .await 点持有它们,就会感染 future,使其变成 !Send。
| |
话虽如此,若该类型的默认用法是瞬时的,并且没有理由跨 .await 边界持有它,则它可以是 !Send。
| |
💡 Send 的代价
理想情况下,应有在工作窃取运行时中为
Send、在每核一线程模型中为!Send的抽象,后者基于Rc、RefCell这类非原子类型。实际上这类抽象并不存在,非原子情形下就无法与 Tokio 兼容。那意味着若要在每核一线程的世界里做成任何事,你都得「重新发明整个世界」。
好消息是,在大多数情况下,原子操作和无竞争锁只有在大约每 64 个字就要访问一次以上时,才会有可测影响。
在热循环里操作大型
Vec<AtomicUsize>是个坏主意,但在原本每核一线程的异步代码里偶尔做一次无竞争原子操作,对性能没有影响,却能换来广泛的生态兼容性。
提供原生逃生舱 (M-ESCAPE-HATCHES)
本条守护:在替代方案出现前,为不受支持的用例提供变通。
包装原生 handle 的类型应提供 unsafe 逃生舱。在互操作场景中,用户可能从别处拿到原生 handle,或必须把你包装后的 handle 经 FFI 传出。为支持这些用例,你应提供 unsafe 转换方法。
| |
不要泄漏外部类型 (M-DONT-LEAK-TYPES)
本条守护:稳定的 API 与较低的长期维护成本。
在可行之处,公开 API 应优先使用 std1 类型,而非来自外部 crate 的类型。例外应慎重考虑。
任何公开 API 中的任何类型,都将成为该 API 契约的一部分。由于默认只随发行附带 std 及其组成 crate,且它们带有永久稳定性保证,因此只有这些类型没有互操作风险。
一个 crate 若暴露另一 crate 的类型,就称为泄漏该类型。
为追求最大的长期稳定性,理论上你的 crate 不应泄漏任何类型。实际上,某些泄漏难以避免,有时甚至有益。我们建议遵循以下启发式规则:
- 若能避免,就不要泄漏第三方类型
- 若你属于伞形 crate,2 则可自由泄漏兄弟 crate 的类型。
- 在相关 feature 标志之后,可以泄漏类型(例如
serde) - 不带 feature 时,仅当能带来实质性收益才可泄漏。最常见的情形是:基于这些类型,与 Rust 生态中其他重要部分实现互操作。
1 在罕见情况下,例如从嵌入式调用的高性能库,你甚至可能要把自己限制在只用
core。2 例如,
runtimecrate 可能是runtime_rt、runtime_app与runtime_clock的伞。由于期望用户只与伞交互,兄弟之间可以互相泄漏类型。
项来自其原始 crate (M-FOREIGN-REEXPORTS)
本条守护:明确无歧义的类型身份。
crate 一般不应再导出其他 crate 的项。例如,若你的 crate 含有方法 foo::download(url: bar::Url),你不应在 foo 内部做 pub use bar::Url。这样可避免上下文中可能出现几十个别名,对用户和智能体都容易混淆,尤其当这些别名与其他 crate 中同名但实际不同的类型混在一起时。
当 crate 接受或返回某个第三方 crate 中定义的类型时,期望用户直接依赖该第三方 crate,并从那里导入该类型。不过本规则有若干合理例外:
- 伞形 crate(参见 M-DONT-LEAK-TYPES)按定义会再导出其他类型
- 因技术原因拆分的 crate(例如从
foo导出foo_core::Url) - 为提供稳定路径而使用宏,例如经由某个隐藏的
foo::__private::Url
可行时接受 impl AsRef<> (M-IMPL-ASREF)
本条守护:调用方可灵活使用自有类型。
在函数签名中,对具有 clear reference hierarchy 的类型,在你不需要取得所有权、或对象创建相对廉价时,接受 impl AsRef<T>。
| 不要用…… | 而应接受…… |
|---|---|
&str、String | impl AsRef<str> |
&Path、PathBuf | impl AsRef<Path> |
&[u8]、Vec<u8> | impl AsRef<[u8]> |
| |
相对地,类型一般不应被这些约束感染:
| |
可行时接受 impl RangeBounds<> (M-IMPL-RANGEBOUNDS)
本条守护:指定范围时灵活且清晰。
接受数字范围的函数必须使用 Range 类型或 trait,而不是手写参数:
| |
此外,能处理任意范围的函数应接受 impl RangeBounds<T>,而不是 Range<T>。
| |
可行时接受 impl 'IO'(sans IO) (M-IMPL-IO)
本条守护:业务逻辑与 I/O 解耦,具备 N×M 可组合性。
只需在初始化期间执行一次性 I/O 的函数和类型,应按 “sans-io” 来写,
并接受某个 impl T(其中 T 是合适的 I/O trait),从而把 I/O 工作外包给另一种类型:
| |
| |
同步函数应使用 std::io::Read 和
std::io::Write。面向不止一个运行时的异步函数应使用
futures::io::AsyncRead 及类似项。
需要执行运行时特定、持续 I/O 的类型应遵循 M-RUNTIME-ABSTRACTED。
