1 个不稳定版本
0.21.0 | 2024年6月13日 |
---|
#71 在 #pyo3
7,885 每月下载量
用于 pyo3-asyncio-0-21
52KB
448 行
PyO3 Asyncio
这是对 pyo3-asyncio
的分支,以提供与 PyO3 0.21 的兼容性。这可能是未来永久分支的基础,具体取决于原始 pyo3-asyncio
维护者的状态。
Rust 绑定 Python 的 Asyncio 库。此存储库促进 Rust Futures 和 Python 协程之间的交互,并管理它们对应的事件循环的生命周期。
PyO3 Asyncio 是 PyO3 生态系统中的 全新 部分。请随时为此存储库打开任何有关功能请求或错误修复的问题。
如果您是新手,阅读以下入门指南是最佳起点!对于 v0.13
和 v0.14
用户,我强烈建议阅读 迁移部分,以了解 v0.14
和 v0.15
中发生了哪些变化。
使用方法
与 PyO3 类似,PyO3 Asyncio 支持以下软件版本
- Python 3.7 及以上(CPython 和 PyPy)
- Rust 1.48 及以上
PyO3 Asyncio 入门
如果您正在使用一个利用异步函数的Python库,或者希望为异步Rust库提供Python绑定,那么pyo3-asyncio
可能正是您需要的工具。它提供了在Python和Rust之间异步函数的转换,并针对流行的Rust运行时(如tokio
和async-std
)提供了优先支持。此外,所有异步Python代码都在默认的asyncio
事件循环上运行,因此pyo3-asyncio
应该能够很好地与现有的Python库协同工作。
在接下来的部分中,我们将对pyo3-asyncio
进行概述,解释如何使用PyO3调用异步Python函数,如何从Python调用异步Rust函数,以及如何配置您的代码库来管理这两个运行时。
快速入门
以下是一些立即开始的示例!这些示例中的概念更详细的说明可以在以下部分中找到。
Rust应用程序
在此,我们初始化运行时,导入Python的asyncio
库,并使用Python的默认EventLoop
和async-std
运行给定的future,直到完成。在future内部,我们将asyncio
睡眠转换为Rust future,并等待它。
# Cargo.toml dependencies
[dependencies]
pyo3 = { version = "0.20" }
pyo3-asyncio-0-21 = { version = "0.20", features = ["attributes", "async-std-runtime"] }
async-std = "1.9"
//! main.rs
use pyo3::prelude::*;
#[pyo3_asyncio_0_21::async_std::main]
async fn main() -> PyResult<()> {
let fut = Python::with_gil(|py| {
let asyncio = py.import_bound("asyncio")?;
// convert asyncio.sleep into a Rust Future
pyo3_asyncio_0_21::async_std::into_future(asyncio.call_method1("sleep", (1.into_py(py),))?)
})?;
fut.await?;
Ok(())
}
该应用程序可以修改为使用tokio
,只需使用#[pyo3_asyncio_0_21::tokio::main]
属性即可。
# Cargo.toml dependencies
[dependencies]
pyo3 = { version = "0.20" }
pyo3-asyncio-0-21 = { version = "0.20", features = ["attributes", "tokio-runtime"] }
tokio = "1.9"
//! main.rs
use pyo3::prelude::*;
#[pyo3_asyncio_0_21::tokio::main]
async fn main() -> PyResult<()> {
let fut = Python::with_gil(|py| {
let asyncio = py.import_bound("asyncio")?;
// convert asyncio.sleep into a Rust Future
pyo3_asyncio_0_21::tokio::into_future(asyncio.call_method1("sleep", (1.into_py(py),))?)
})?;
fut.await?;
Ok(())
}
有关此库的更多详细信息,请参阅API文档和以下简介。
PyO3原生Rust模块
PyO3 Asyncio还可以用来编写具有异步函数的原生模块。
将[lib]
部分添加到Cargo.toml
中,使您的库成为Python可以导入的cdylib
。
[lib]
name = "my_async_module"
crate-type = ["cdylib"]
通过启用extension-module
功能,使您的项目依赖于pyo3
,并选择您的pyo3-asyncio
运行时。
对于async-std
[dependencies]
pyo3 = { version = "0.20", features = ["extension-module"] }
pyo3-asyncio-0-21 = { version = "0.20", features = ["async-std-runtime"] }
async-std = "1.9"
对于tokio
[dependencies]
pyo3 = { version = "0.20", features = ["extension-module"] }
pyo3-asyncio-0-21 = { version = "0.20", features = ["tokio-runtime"] }
tokio = "1.9"
导出一个使用async-std
的异步函数
//! lib.rs
use pyo3::{prelude::*, wrap_pyfunction};
#[pyfunction]
fn rust_sleep(py: Python) -> PyResult<Bound<PyAny>> {
pyo3_asyncio_0_21::async_std::future_into_py(py, async {
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
Ok(())
})
}
#[pymodule]
fn my_async_module(py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(rust_sleep, m)?)?;
Ok(())
}
如果您想使用tokio
,那么您的模块应该看起来像这样
//! lib.rs
use pyo3::{prelude::*, wrap_pyfunction};
#[pyfunction]
fn rust_sleep(py: Python) -> PyResult<Bound<PyAny>> {
pyo3_asyncio_0_21::tokio::future_into_py(py, async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(())
})
}
#[pymodule]
fn my_async_module(py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(rust_sleep, m)?)?;
Ok(())
}
您可以使用maturin构建您的模块(请参阅PyO3指南中的在Python中使用Rust部分以获取设置说明)。之后,您应该能够运行Python REPL来尝试它。
maturin develop && python3
🔗 Found pyo3 bindings
🐍 Found CPython 3.8 at python3
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Python 3.8.5 (default, Jan 27 2021, 15:41:15)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>>
>>> from my_async_module import rust_sleep
>>>
>>> async def main():
>>> await rust_sleep()
>>>
>>> # should sleep for 1s
>>> asyncio.run(main())
>>>
在Rust中等待异步Python函数
让我们看看一个简单的异步Python函数
# Sleep for 1 second
async def py_sleep():
await asyncio.sleep(1)
Python中的异步函数简单地是返回一个coroutine
对象的函数。对于我们来说,我们真的不需要知道太多关于这些coroutine
对象。关键因素在于,调用异步函数就像调用一个普通函数一样,唯一的区别是我们必须对它返回的对象做些特别的事情。
通常在Python中,这特别的事情就是await
关键字,但为了在Rust中等待这个coroutine,我们首先需要将其转换为Rust版本的coroutine
:一个Future
。这就是pyo3-asyncio
发挥作用的地方。函数pyo3_asyncio_0_21::into_future
为我们执行了这个转换。
use pyo3::prelude::*;
#[pyo3_asyncio_0_21::tokio::main]
async fn main() -> PyResult<()> {
let future = Python::with_gil(|py| -> PyResult<_> {
// import the module containing the py_sleep function
let example = py.import_bound("example")?;
// calling the py_sleep method like a normal function
// returns a coroutine
let coroutine = example.call_method0("py_sleep")?;
// convert the coroutine into a Rust future using the
// tokio runtime
pyo3_asyncio_0_21::tokio::into_future(coroutine)
})?;
// await the future
future.await?;
Ok(())
}
如果您想了解更多关于协程(
coroutines
)和通用可等待对象(awaitables
)的信息,请查看Python 3asyncio
文档以获取更多信息。
在 Python 中等待 Rust 未来
这里我们有之前用 Rust 编写的相同的异步函数,使用的是async-std
运行时
/// Sleep for 1 second
async fn rust_sleep() {
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
}
类似于 Python,Rust 的异步函数也返回一个特殊对象,称为 Future
let future = rust_sleep();
我们可以将这个 Future
对象转换为 Python,使其变为可等待的。这告诉 Python 可以使用 await
关键字。为了实现这一点,我们将调用pyo3_asyncio_0_21::async_std::future_into_py
use pyo3::prelude::*;
async fn rust_sleep() {
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
}
#[pyfunction]
fn call_rust_sleep(py: Python) -> PyResult<Bound<PyAny>> {
pyo3_asyncio_0_21::async_std::future_into_py(py, async move {
rust_sleep().await;
Ok(())
})
}
在 Python 中,我们可以像调用任何其他异步函数一样调用这个 pyo3 函数
from example import call_rust_sleep
async def rust_sleep():
await call_rust_sleep()
管理事件循环
Python 的事件循环需要一些特殊处理,特别是在主线程方面。Python 的某些 asyncio
功能,如适当的信号处理,需要控制主线程,而这通常与 Rust 不兼容。
幸运的是,Rust 的事件循环相当灵活,并不需要控制主线程,因此,在 pyo3-asyncio
中,我们决定处理 Rust/Python 互操作的最佳方式是将主线程交给 Python,并在后台运行 Rust 的事件循环。不幸的是,由于大多数事件循环实现都 更倾向于 控制主线程,这仍然可能导致一些问题。
PyO3 Asyncio 初始化
由于 Python 需要控制主线程,我们无法使用 Rust 运行时提供的方便的 proc macros 来处理 main
函数或 #[test]
函数。相反,PyO3 的初始化必须从 main
函数开始,并且主线程必须在pyo3_asyncio_0_21::run_forever
或 pyo3_asyncio_0_21::async_std::run_until_complete
上阻塞。
由于我们必须在这些函数上阻塞,因此我们不能使用 #[async_std::main]
或 #[tokio::main]
,因为在异步函数中进行长时间阻塞调用不是一个好主意。
内部,这些
#[main]
proc macros 被展开成类似这样的形式fn main() { // your async main fn async fn _main_impl() { /* ... */ } Runtime::new().block_on(_main_impl()); }
在由
block_on
驱动的Future
中执行长时间阻塞调用,会阻止该线程执行其他操作,并可能对某些运行时造成问题(实际上,这也会使单线程运行时出现死锁!)。许多运行时都有某种spawn_blocking
机制可以避免这个问题,但同样,这里我们不能使用它,因为我们需要在 主 线程上阻塞。
因此,pyo3-asyncio
提供了自己的一组进程宏,以提供这种初始化。这些宏旨在模仿 async-std
和 tokio
的初始化,同时满足 Python 运行时的需求。
以下是使用 async-std
运行时进行 PyO3 初始化的完整示例
use pyo3::prelude::*;
#[pyo3_asyncio_0_21::async_std::main]
async fn main() -> PyResult<()> {
// PyO3 is initialized - Ready to go
let fut = Python::with_gil(|py| -> PyResult<_> {
let asyncio = py.import_bound("asyncio")?;
// convert asyncio.sleep into a Rust Future
pyo3_asyncio_0_21::async_std::into_future(
asyncio.call_method1("sleep", (1.into_py(py),))?
)
})?;
fut.await?;
Ok(())
}
关于 asyncio.run
的说明
在 Python 3.7+ 中,使用 asyncio
运行顶层协程的推荐方法是使用 asyncio.run
。在 v0.13
中,由于初始化问题,我们不建议使用此函数,但在 v0.14
中,使用此函数是完全可以接受的...但有前提。
由于我们的 Rust <--> Python 转换需要一个指向 Python 事件循环的引用,这带来了问题。想象一下,我们有一个 PyO3 Asyncio 模块,其中定义了一个类似于之前示例中的 rust_sleep
函数。你可能合理地认为,你可以像这样直接将其传递给 asyncio.run
import asyncio
from my_async_module import rust_sleep
asyncio.run(rust_sleep())
你可能会惊讶地发现,这会抛出一个错误
Traceback (most recent call last):
File "example.py", line 5, in <module>
asyncio.run(rust_sleep())
RuntimeError: no running event loop
这里发生的情况是我们调用 rust_sleep
在 实际上在由 asyncio.run
创建的事件循环上运行未来之前。这不符合直觉,但这是预期的行为,不幸的是,似乎没有很好的方法在 PyO3 Asyncio 本身内解决这个问题。
然而,我们可以通过一个简单的解决方案使此示例工作
import asyncio
from my_async_module import rust_sleep
# Calling main will just construct the coroutine that later calls rust_sleep.
# - This ensures that rust_sleep will be called when the event loop is running,
# not before.
async def main():
await rust_sleep()
# Run the main() coroutine at the top-level instead
asyncio.run(main())
非标准 Python 事件循环
Python 允许你使用除默认的 asyncio
事件循环之外的其他选择。一种流行的替代方案是 uvloop
。在 v0.13
中,使用非标准事件循环是一段痛苦的经历,但在 v0.14
中,这是微不足道的。
在 PyO3 Asyncio 原生扩展中使用 uvloop
# Cargo.toml
[lib]
name = "my_async_module"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.20", features = ["extension-module"] }
pyo3-asyncio-0-21 = { version = "0.20", features = ["tokio-runtime"] }
async-std = "1.9"
tokio = "1.9"
//! lib.rs
use pyo3::{prelude::*, wrap_pyfunction};
#[pyfunction]
fn rust_sleep(py: Python) -> PyResult<Bound<PyAny>> {
pyo3_asyncio_0_21::tokio::future_into_py(py, async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(())
})
}
#[pymodule]
fn my_async_module(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(rust_sleep, m)?)?;
Ok(())
}
$ maturin develop && python3
🔗 Found pyo3 bindings
🐍 Found CPython 3.8 at python3
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Python 3.8.8 (default, Apr 13 2021, 19:58:26)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> import uvloop
>>>
>>> import my_async_module
>>>
>>> uvloop.install()
>>>
>>> async def main():
... await my_async_module.rust_sleep()
...
>>> asyncio.run(main())
>>>
在 Rust 应用程序中使用 uvloop
在 Rust 应用程序中使用 uvloop
稍微复杂一些,但通过相对较少的修改仍然是可能的。
不幸的是,我们无法在非标准事件循环中使用 #[pyo3_asyncio_0_21::<runtime>::main]
属性。这是因为 #[pyo3_asyncio_0_21::<runtime>::main]
进程宏必须在与我们可以安装 uvloop
策略之前与 Python 事件循环交互。
[dependencies]
async-std = "1.9"
pyo3 = "0.20"
pyo3-asyncio-0-21 = { version = "0.20", features = ["async-std-runtime"] }
//! main.rs
use pyo3::{prelude::*, types::PyType};
fn main() -> PyResult<()> {
pyo3::prepare_freethreaded_python();
Python::with_gil(|py| {
let uvloop = py.import_bound("uvloop")?;
uvloop.call_method0("install")?;
// store a reference for the assertion
let uvloop = PyObject::from(uvloop);
pyo3_asyncio_0_21::async_std::run(py, async move {
// verify that we are on a uvloop.Loop
Python::with_gil(|py| -> PyResult<()> {
assert!(uvloop
.bind(py)
.getattr("Loop")?
.downcast::<PyType>()
.unwrap()
.is_instance(&pyo3_asyncio_0_21::async_std::get_current_loop(py)?)?);
Ok(())
})?;
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
Ok(())
})
})
}
更多信息
- 使用pyo3-asyncio管理事件循环引用可能有些棘手。请参阅API文档中的事件循环引用和ContextVars,以更好地了解该库中事件循环引用的管理。
- 测试pyo3-asyncio库和应用需要自定义测试工具,因为Python需要控制主线程。您可以在testing模块的API文档中找到测试指南。
迁移指南
从0.13迁移到0.14
那么从v0.13
到v0.14
有哪些变化?
实际上,变化很多。在v0.13
的初始化行为中存在一些相当严重的缺陷。虽然最好是在不更改公共API的情况下解决这些问题,但我决定最好是破坏一些旧的API,而不是完全改变现有函数的底层行为。我明白这可能会有些头疼,所以希望这一部分能帮您顺利过渡。
为了使事情变得简单一些,我决定保留大部分旧的API,同时提供新的API(并带有一些弃用警告,以鼓励用户远离它)。应该可以在使用v0.13
API的同时使用更新的v0.14
API,这样您就可以分阶段而不是一次性升级您的应用程序。
在您开始之前,我建议您先看看事件循环引用和ContextVars,以便更好地了解这些变化的动机以及使用新转换的细微差别。
0.14 突出亮点
- 现在Tokio初始化是懒加载的。
- 如果您使用的是多线程调度器,则无需配置。
- 对
pyo3_asyncio_0_21::tokio::init_multithread
或pyo3_asyncio_0_21::tokio::init_multithread_once
的调用可以删除。 - 对
pyo3_asyncio_0_21::tokio::init_current_thread
或pyo3_asyncio_0_21::tokio::init_current_thread_once
的调用需要特别注意。 - 通过将一个
tokio::runtime::Builder
传递给pyo3_asyncio_0_21::tokio::init
,而不是一个tokio::runtime::Runtime
来执行自定义运行时配置。
- 已经添加了一组新的、更正确的方法来替换
v0.13
转换。pyo3_asyncio_0_21::into_future_with_loop
pyo3_asyncio_0_21::<runtime>::future_into_py_with_loop
pyo3_asyncio_0_21::<runtime>::local_future_into_py_with_loop
pyo3_asyncio_0_21::<runtime>::into_future
pyo3_asyncio_0_21::<runtime>::future_into_py
pyo3_asyncio_0_21::<runtime>::local_future_into_py
pyo3_asyncio_0_21::<runtime>::get_current_loop
pyo3_asyncio_0_21::try_init
在仅使用0.14
转换的情况下不再需要。ThreadPoolExecutor
在启动时不再自动配置。- 幸运的是,这似乎对
v0.13
代码影响不大,只是意味着现在可以手动配置执行器,按照您的需求进行设置。
- 幸运的是,这似乎对
将代码升级到 0.14
-
修复 PyO3 0.14 初始化问题。
- PyO3 0.14 将其自动初始化行为设置为“auto-initialize”后门。您可以在项目中启用“auto-initialize”行为,或将
pyo3::prepare_freethreaded_python()
调用添加到程序开头。 - 如果您使用
#[pyo3_asyncio_0_21::<runtime>::main]
proc 宏属性,则可以跳过此步骤。#[pyo3_asyncio_0_21::<runtime>::main]
会在启动时调用pyo3::prepare_freethreaded_python()
,无论您的项目的“auto-initialize”功能是否启用。
- PyO3 0.14 将其自动初始化行为设置为“auto-initialize”后门。您可以在项目中启用“auto-initialize”行为,或将
-
修复 tokio 初始化。
-
对
pyo3_asyncio_0_21::tokio::init_multithread
或pyo3_asyncio_0_21::tokio::init_multithread_once
的调用可以删除。 -
如果您使用的是当前线程调度器,则需要手动在初始化期间启动它所运行的线程。
let mut builder = tokio::runtime::Builder::new_current_thread(); builder.enable_all(); pyo3_asyncio_0_21::tokio::init(builder); std::thread::spawn(move || { pyo3_asyncio_0_21::tokio::get_runtime().block_on( futures::future::pending::<()>() ); });
-
可以将自定义的
tokio::runtime::Builder
配置传递给pyo3_asyncio_0_21::tokio::init
。第一次调用pyo3_asyncio_0_21::tokio::get_runtime()
时会惰性地实例化tokio::runtime::Runtime
。
-
-
如果您在应用程序中使用
pyo3_asyncio_0_21::run_forever
,则应切换到更手动的方法。run_forever
不是在 Python 中运行事件循环的推荐方式,因此可能最好摆脱它。这个函数需要为0.14
进行更改,但由于它被认为是一个边缘情况,因此决定如果用户需要,可以手动调用它。use pyo3::prelude::*; fn main() -> PyResult<()> { pyo3::prepare_freethreaded_python(); Python::with_gil(|py| { let asyncio = py.import_bound("asyncio")?; let event_loop = asyncio.call_method0("new_event_loop")?; asyncio.call_method1("set_event_loop", (&event_loop,))?; let event_loop_hdl = PyObject::from(event_loop.clone()); pyo3_asyncio_0_21::tokio::get_runtime().spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(1)).await; // Stop the event loop manually Python::with_gil(|py| { event_loop_hdl .bind(py) .call_method1( "call_soon_threadsafe", (event_loop_hdl .bind(py) .getattr("stop") .unwrap(),), ) .unwrap(); }) }); event_loop.call_method0("run_forever")?; Ok(()) }) }
-
用它们的较新版本替换转换。
您可能会遇到有关使用
get_running_loop
与get_event_loop
的一些问题。有关这些较新转换的详细信息及其使用方法,请参阅 事件循环引用和 ContextVars。- 用
pyo3_asyncio_0_21::into_future
替换为pyo3_asyncio_0_21::<runtime>::into_future
- 用
pyo3_asyncio_0_21::<runtime>::into_coroutine
替换为pyo3_asyncio_0_21::<runtime>::future_into_py
- 将
pyo3_asyncio_0_21::get_event_loop
替换为pyo3_asyncio_0_21::<runtime>::get_current_loop
- 用
-
在所有转换都被替换为它们的
v0.14
对应项之后,可以安全地删除pyo3_asyncio_0_21::try_init
。
版本
v0.15
中已删除v0.13
API。
从 0.14 迁移到 0.15+
为了支持从 Python 正确取消以及 contextvars
模块,API 进行了一些更改。
-
任何
cancellable_future_into_py
和local_cancellable_future_into_py
转换都可以用它们的future_into_py
和local_future_into_py
对应项替换。取消支持在 0.15 中成为默认行为。
-
应该用新的
*_with_locals
转换替换*_with_loop
转换。use pyo3::prelude::*; Python::with_gil(|py| -> PyResult<()> { // *_with_loop conversions in 0.14 // // let event_loop = pyo3_asyncio_0_21::get_running_loop(py)?; // // let fut = pyo3_asyncio_0_21::tokio::future_into_py_with_loop( // event_loop, // async move { Ok(Python::with_gil(|py| py.None())) } // )?; // // should be replaced with *_with_locals in 0.15+ let fut = pyo3_asyncio_0_21::tokio::future_into_py_with_locals( py, pyo3_asyncio_0_21::tokio::get_current_locals(py)?, async move { Ok(()) } )?; Ok(()) });
-
scope
和scope_local
变体现在接受TaskLocals
而不是event_loop
。通常只需将event_loop
替换为pyo3_asyncio_0_21::TaskLocals::new(event_loop).copy_context(py)?
。 -
future_into_py
、future_into_py_with_locals
、local_future_into_py
和local_future_into_py_with_locals
的返回类型现在受限于绑定IntoPy<PyObject>
而不是要求返回类型为PyObject
。这可以使将来的返回类型更加灵活,但有时当具体类型不明确时(例如使用into()
)推理可能会失败。有时可以简单地删除into()
。 -
run
和run_until_complete
现在可以返回任何Send + 'static
值。
从 0.15 迁移到 0.16
实际上,API 中变化不大。我很高兴地说,PyO3 Asyncio 在 0.16 中达到了相当稳定的状态。大部分是关于清理和从 API 中移除已弃用的函数。
PyO3 0.16 带来了一些自己的 API 变更,但对其影响最大的变化是决定停止支持 Python 3.6。PyO3 Asyncio 使用了一些针对 Python 3.7 之前版本的 asyncio 库的替代方案/黑客攻击,现在不再必要。因此,PyO3 Asyncio 的底层实现现在更加干净。
PyO3 Asyncio 0.15 包含了一些对 API 的重要修复,以添加对正确任务取消的支持,并允许在 Python 协程中保留/使用 contextvars。这导致一些用于边缘情况的 0.14 函数被弃用,并改为使用更正确版本的函数,这些已弃用的函数现在已从 API 中删除。
此外,随着PyO3 Asyncio 0.16的发布,该库现在对将Python的异步生成器转换为Rust的Stream
提供了实验性支持。目前有两个版本:v1
和v2
,它们的性能和类型签名略有不同,所以我希望得到一些反馈,看看哪一个更适合下游用户。只需启用unstable-streams
功能即可!
逆转换,即从Rust的
Stream
到Python的异步生成器,如果需求的话,可能在未来的版本中实现!
依赖项
~1.5MB
~35K SLoC