├── LICENSE └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Harrison Burt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Async-PyO3-Examples 2 | A repo to help people to interact with AsyncIO with PyO3 native extensions. 3 | 4 | # Contents 5 | 6 | - **[Introduction](https://github.com/ChillFish8/Async-PyO3-Examples/blob/main/README.md#introduction---a-word-of-warning)** *If you're new to this book you will want to read this* 7 | - **[Breaking Down Python](https://github.com/ChillFish8/Async-PyO3-Examples/blob/main/README.md#breaking-down-python---how-asyncio-works-in-python-under-the-hood)** *Goes through the internals of how AsyncIO with async and await is implemented* 8 | - **[Implementing Rust](https://github.com/ChillFish8/Async-PyO3-Examples/blob/main/README.md#implementing-rust---in-progess)** *Takes what we learnt from [Breaking Down Python]() and applies it to our Rust code* - **In Development** 9 | 10 | 11 | ## Introduction - A Word Of Warning 12 | Please read through these warnings before jumping into this, asyncio at the low level is fairly complicated and is definitley **not** recommended for begginers who have little to no understanding of AsyncIO. 13 | 14 | #### Writing coroutines in Rust and Pyo3 will not make them 'faster' 15 | creating and running the coroutine will actually be *slower* on average so please do not enter this believing that if you write all your coroutines in Rust just to gain execution speed, They'll only be faster if what you plan todo inside them can be sped up by Rust but *need* to be coroutines in order to achieve that. 16 | 17 | #### Low level coroutines are *not* simple 18 | No matter how you look at it coroutines in PyO3 will not be easy to write, you have no syntactic sugar methods like `async` and `await` (The Rust future implementations do not work). 19 | You will also be without yeild from (under the hood `await`) so you should think of how you are going to structure your code going into it. 20 | 21 | 22 | ## Breaking Down Python - How AsyncIO works in Python under the hood 23 | Python's AsyncIO is built off the idea of a event loop and generator coroutines allowing functions to be suspended and resumed which is managed by the event loop when a `await` block is hit. This gives us the affect of single threaded concurrency. 24 | 25 | ### Breaking down async/await 26 | Since Python 3.5.5 synatic sugar methods of `async` and `await` make our lives alot easier by doing all the heavy lifting for us keeping all the messy internals hidden away. 27 | However, this wasn't always a thing. In Python versions older than 3.5.5 we were left with manually decorating our functions and using `yield from` (more on this later), if you've worked with async in Python 2 or old versions of Python 3 this might come easier to you. 28 | 29 | #### What is `await` under the hood? 30 | await is basically a `yield from` wrapper after calling the coroutine and then calling `__await__`, which is all we really need to know for the sake of writing async extensions in Rust, the next question you will probably have is "well... What's yield from?", glad you asked! All we need to know about yield from is that we yield the results from another iterator until it's exhuased (All coroutines are generators so all support being iterated over which will support yield from) however, we cannot use `yield from` in our Rust code so here's what we would do logically in Rust (Implemented in Python). 31 | 32 | **Example code:** 33 | ```py 34 | # python 3.8 35 | 36 | # We defined our coroutine using the normal async system for now, 37 | # we should see "being called" and then "hello" being returned by utilising 38 | # yield from. 39 | async def foo(): 40 | print("foo is being called") 41 | return "hello" 42 | 43 | # We can still call our coroutine like normal 44 | my_iterator = foo() 45 | 46 | # We have to call __await__ as part of the async API 47 | my_iterator = my_iterator.__await__() 48 | 49 | # We have to call __iter__ to expose the 50 | # generator aspect of the coroutine. 51 | my_iterator = my_iterator.__iter__() 52 | 53 | # Because of the generator nature of coroutines, 54 | # we expect it to raise a StopIteration error which 55 | # gives us our resulting value. 56 | try: 57 | while True: # Keep going till it errors 58 | next(my_iterator) 59 | except StopIteration as result: 60 | print(f"Result of our coroutine: {repr(result.value)}") 61 | ``` 62 | 63 | **Output:** 64 | ``` 65 | >>> foo is being called 66 | >>> Result of our coroutine: 'hello' 67 | ``` 68 | 69 | #### What is `async def` under the hood? 70 | `async def` or `async` key words are alot more complicated under the hood than `await`. 71 | Before `async` became a keyword you would have to decorate your function with `@asyncio.coroutine` to wrap your code in a coroutine class, however we have to re-create the coroutine class itself. 72 | 73 | **A coroutine remake in Python** 74 | ```py 75 | # python 3.8 76 | import asyncio 77 | 78 | 79 | # We're going to try recreate this coroutine by 80 | # making a class with the required dunder methods. 81 | # 82 | # This coroutine just returns the parameter it's given. 83 | async def my_coroutine(arg1): 84 | return arg1 85 | 86 | 87 | # Our coroutine copy takes arg1 just like my_coroutine, 88 | # we can save this to somewhere, in this case we're setting it 89 | # to arg1 as a instance variable. 90 | class MyCoroutineCopy: 91 | def __init__(self, arg1): 92 | self.arg1 = arg1 93 | 94 | # __await__ is required to register with the `await` keyword returning self. 95 | def __await__(self): 96 | return self 97 | 98 | # __iter__ is used just to return a iterator, we dont need this to be self 99 | # but for the sake of this we're using the class itself as a iterator. 100 | def __iter__(self): 101 | return self 102 | 103 | # __next__ is used to make our coroutine class a generator which can either 104 | # return to symbolise a yield or raise StopIteration(value) to return the 105 | # output of the coroutine 106 | def __next__(self): 107 | # we'll do what my_coroutine does and echo the first parameter 108 | raise StopIteration(self.arg1) 109 | 110 | 111 | async def main(): 112 | result = await my_coroutine("foo") 113 | print(f"my_coroutine returned with: {repr(result)}") 114 | 115 | result = await MyCoroutineCopy("foo") 116 | print(f"MyCoroutineCopy returned with: {repr(result)}") 117 | 118 | 119 | asyncio.run(main()) 120 | ``` 121 | 122 | **Output:** 123 | ``` 124 | my_coroutine returned with: 'foo' 125 | MyCoroutineCopy returned with: 'foo' 126 | ``` 127 | 128 | ## Implementing Rust - IN PROGESS 129 | Now we've got all the concepts out of the way and under our tool belt we can actually start to recreate this is Rust using PyO3. 130 | 131 | We'll start by breaking down and recreating our first Python example recreating a `await`: 132 |

133 | ### Re-Creating Python's `await` in Rust 134 | **Setting up boilerplate:** 135 | This is just our standard bit of setup, if you already have a existing bit of code with this in you can ignore it. 136 | Though for the purposes of learning it would be a good idea to have a seperate area to mess with before putting it in your actual code. 137 | 138 | ```rust 139 | // lets get our basics setup first 140 | use pyo3::prelude::*; 141 | use pyo3::wrap_pyfunction; 142 | 143 | 144 | // We're going to create a basic function just to house our awaiting 145 | #[pyfunction] 146 | fn await_foo(py: Python, foo: PyObject) -> PyResult<()> { 147 | // We'll add our code here soon 148 | } 149 | 150 | 151 | // Lets just call our module await_from_rust for simplicity. 152 | #[pymodule] 153 | fn await_from_rust(_py: Python, m: &PyModule) -> PyResult<()> { 154 | m.add_function(wrap_pyfunction!(await_foo, m)?).unwrap(); 155 | Ok(()) 156 | } 157 | ``` 158 | 159 | **Producing our iterator from a coroutine:** 160 | A few things differ when we want to setup our coroutine call in Rust because we can make use of the C api directly which can help with efficency. 161 | `call_method`'s are generally quite expensive calls so reducing them where we can has the potential to improve our performance. 162 | 163 | Our code to produce the iterator, notice how we use `iter()` instead of `call_method0(py, "__iter__")`, 164 | always a good idea to check the docs to see if the thing you're doing supports direct calls rather than going through `call_method`. 165 | ```rust 166 | // We call it like we did in python 167 | let mut my_iterator = foo.call0(py)?; 168 | 169 | // We call __await__ on the coroutine using call_method0 for no parameters 170 | // as part of the async API 171 | my_iterator = my_iterator.call_method0(py, "__await__")?; 172 | 173 | // We call __iter__ on the coroutine using call_method0 for no parameters 174 | // to expose the generator aspect of the coroutine 175 | my_iterator = my_iterator.iter()?; 176 | ``` 177 | 178 | **actually using our iterator** 179 | Now we have our iterator we can use `loop` to call next until it's exhuasted. Unlike Python we dont have try/except (not in the same way atleast) so our loop is positioned slightly diffrent than our Python example. 180 | 181 | ```rust 182 | // Unlike python we cant wrap this in a try except / we dont want to. 183 | // so for this we'll just use loop to iterate over it like While True 184 | // and match the result. 185 | let mut result: PyResult; // Saves us constantly re-declaring it. 186 | loop { 187 | 188 | // We call __next__ which is all the next() function does in Python. 189 | result = my_iterator.call_method0(py, "__next__"); 190 | 191 | // lets match if the iterator has returned or raised StopIteration. 192 | // For this example we are assuming that no other error will ever be 193 | // raised for the sake of simplicity. 194 | match result { 195 | // If its okay (it returned normally) we call next again. 196 | Ok(r) => continue, 197 | 198 | // if it errors we know we're done. 199 | Err(e) => { 200 | if let Ok(stop_iteration) = e.pvalue(py).downcast::() { 201 | let returned_value = stop_iteration.getattr("value")?; 202 | 203 | // Let's display that result of ours 204 | println!("Result of our coroutine: {:?}", returned_value); 205 | 206 | // Time to escape from this while loop. 207 | return Ok(()); 208 | } else { 209 | return Err(e); 210 | } 211 | 212 | } 213 | }; 214 | } 215 | ``` 216 | 217 | 218 | **our finished function that `awaits` a coroutine:** 219 | ```rust 220 | // lets get our basics setup first 221 | use pyo3::prelude::*; 222 | use pyo3::wrap_pyfunction; 223 | 224 | 225 | // We're going to create a basic function just to house our awaiting 226 | #[pyfunction] 227 | fn await_foo(py: Python, foo: PyObject) -> PyResult<()> { 228 | 229 | // We call it like we did in python 230 | let mut my_iterator = foo.call0(py)?; 231 | 232 | // We call __await__ on the coroutine using call_method0 for no parameters 233 | // as part of the async API 234 | my_iterator = my_iterator.call_method0(py, "__await__")?; 235 | 236 | // We call __iter__ on the coroutine using call_method0 for no parameters 237 | // to expose the generator aspect of the coroutine 238 | my_iterator = my_iterator.iter()?; 239 | 240 | // Unlike python we cant wrap this in a try except / we dont want to. 241 | // so for this we'll just use loop to iterate over it like While True 242 | // and match the result. 243 | let mut result: PyResult; // Saves us constantly re-declaring it. 244 | loop { 245 | 246 | // We call __next__ which is all the next() function does in Python. 247 | result = my_iterator.next(); 248 | 249 | // lets match if the iterator has returned or raised StopIteration. 250 | // For this example we are assuming that no other error will ever be 251 | // raised for the sake of simplicity. 252 | match result { 253 | // If its okay (it returned normally) we call next again. 254 | Ok(r) => continue, 255 | 256 | // if it errors we know we're done. 257 | Err(e) => { 258 | if let Ok(stop_iteration) = e.pvalue(py).downcast::() { 259 | let returned_value = stop_iteration.getattr("value")?; 260 | 261 | // Let's display that result of ours 262 | println!("Result of our coroutine: {:?}", returned_value); 263 | 264 | // Time to escape from this while loop. 265 | return Ok(()); 266 | } else { 267 | return Err(e); 268 | } 269 | 270 | } 271 | }; 272 | } 273 | 274 | // Rust will warn that this is unreachable, probably a good idea to 275 | // add a timeout in your actual code. 276 | Ok(()) 277 | } 278 | 279 | #[pymodule] 280 | fn await_from_rust(_py: Python, m: &PyModule) -> PyResult<()> { 281 | m.add_function(wrap_pyfunction!(await_foo, m)?).unwrap(); 282 | Ok(()) 283 | } 284 | ``` 285 |
286 | 287 | ### Re-Creating Python's `async def`/coroutines in Rust 288 | Great! Now we've managed to `await` our coroutines and understand that concept we can start to build our own coroutines.
289 | Now this is where it starts to get a little bit more complicated because we are without `yield` or `yield from` directly, 290 | however, good news! PyO3 already implements some helper functions to make our life a little bit easier with a `yield` function and `return` function for iterators. 291 | Other than that we need to implement the requrires dunder methods (double underscore methods e.g `__init__`) this cannot be done via simply making a function called it unless specificially stated. 292 |

293 | 294 | We're going to use the `#[pyproto]` macro for a couple things: 295 | - For implementing `__await__` by implying the `PyAsyncProtocol` 296 | - For implementing the `__iter__` and `__next__` methods by implying the `PyIterProtocol`, see [the docs for more about iterators in PyO3](https://pyo3.rs/v0.12.3/class/protocols.html?highlight=Iter#iterator-types) 297 |

298 | 299 | **Setting up our boilerplate:**
300 | Like we did with `await` we need to setup some basic boiler plate for the sake of demonstatration. 301 | 302 | Our struct `MyCoroutine` will form the bases for our awaitable, it is important to note now that this will not be identified as a `coroutine` type by Python but as a awaitable type instead. This will mean things like `asyncio.create_task()` wont work but `asyncio.ensure_future()` will. 303 | 304 | ```rust 305 | // lets get our basics setup first 306 | use pyo3::prelude::*; 307 | 308 | // Our base Python class that will do the job of Python's 309 | // coroutine class. 310 | #[pyclass] 311 | struct MyCoroutine {} 312 | 313 | // Lets just call our module await_from_rust for simplicity. 314 | #[pymodule] 315 | fn await_from_rust(_py: Python, m: &PyModule) -> PyResult<()> { 316 | m.add_class::()?; 317 | Ok(()) 318 | } 319 | ``` 320 | 321 | **Making it 'awaitable'**
322 | Python has a very simple system for making a object awaitable, simply `await` calls `__await__` under the hood, we can recreate this using the `pyproto` macro and the `PyAsyncProtocol`. 323 | 324 | ```rust 325 | // lets get our basics setup first 326 | use pyo3::prelude::*; 327 | 328 | // Our base Python class that will do the job of Python's 329 | // coroutine class. 330 | #[pyclass] 331 | struct MyCoroutine {} 332 | 333 | // Adding out async protocol, this makes use awaitable. 334 | // it should be important to note: DO NOT LISTEN TO YOUR LINTER. 335 | // Your linter can get very, very confused at this system and will 336 | // want you to implement things that you do *not* want to implement 337 | // to 'satisfy' this protocol implementation. 338 | // even if it highlights in red the bellow implementation will stil compile. 339 | #[pyproto] 340 | impl PyAsyncProtocol for MyCoroutine { 341 | fn __await__(slf: PyRef) -> PyRef { 342 | slf // We're saying that we are the iterable part of the coroutine. 343 | } 344 | } 345 | 346 | // Lets just call our module await_from_rust for simplicity. 347 | #[pymodule] 348 | fn await_from_rust(_py: Python, m: &PyModule) -> PyResult<()> { 349 | m.add_class::()?; 350 | Ok(()) 351 | } 352 | ``` 353 | 354 | *Wow! is it that easy to make a coroutine?* - Sadly not quite, the `slf` reference does not allow us to internally call functions we've defined, in the above example as well we are telling Python that our iterable is itself. This will crash if we try to run this on Python now because we're missing the iterator protocol. 355 | 356 | However, this simple setup still carries alot of use. If you have something that just needs to be awaitable and transfer some pre-computed fields to a existing awaitable or PyObject we can just create the object -> call `__await__` and return that. This can make things considerably easier if your Rust coroutines are simply acting as a middle man for some efficent code. 357 | 358 | **Making our awaitable a iterable**
359 | It should be important to note that just because something is awaitable does not make it a coroutine, coroutines are essentially self contained classes that return `self` on both `__await__` and `__iter__` calls and execute the actual code upon the `__next__` call (Please note I am heavily simplifying it to make sense of the following Rust code.) 360 | 361 | Just like we did with `__await__` we can use `pyproto` to implement the iterable dunder methods: 362 | ```rust 363 | // lets get our basics setup first 364 | use pyo3::prelude::*; 365 | use pyo3::iter::IterNextOutput; 366 | use pyo3::PyIterProtocol; 367 | use pyo3::PyAsyncProtocol; 368 | use pyo3::wrap_pyfunction; 369 | 370 | // Our base Python class that will do the job of Python's 371 | // coroutine class. 372 | #[pyclass] 373 | struct MyCoroutine {} 374 | 375 | // Adding out async protocol, this makes use awaitable. 376 | #[pyproto] 377 | impl PyAsyncProtocol for MyCoroutine { 378 | fn __await__(slf: PyRef) -> PyRef { 379 | slf // We're saying that we are the iterable part of the coroutine. 380 | } 381 | } 382 | 383 | #[pyproto] 384 | impl PyIterProtocol for MyCoroutine { 385 | // This is a optional function, if you dont want todo anything like returning a existing iterator 386 | // dont worry about implementing this. 387 | fn __iter__(slf: PyRef) -> PyRef { 388 | slf 389 | } 390 | 391 | // There are other return types you can give however IterNextOutput is by far the biggest 392 | // helper you will get when making coroutines. 393 | fn __next__(_slf: PyRefMut) -> IterNextOutput, &'static str> { 394 | IterNextOutput::Return("Ended") 395 | } 396 | } 397 | 398 | // Exposing our custom awaitable to Python. 399 | // This will behave like a coroutine. 400 | #[pyfunction] 401 | fn my_coroutine() -> MyCoroutine { 402 | MyCoroutine {} 403 | } 404 | 405 | // Lets just call our module await_from_rust for simplicity. 406 | #[pymodule] 407 | fn await_from_rust(_py: Python, m: &PyModule) -> PyResult<()> { 408 | m.add_class::()?; 409 | m.add_function(wrap_pyfunction!(my_coroutine, m)?).unwrap(); 410 | Ok(()) 411 | } 412 | ``` 413 | 414 | And now we can call our Rust coroutine from Python. 415 | 416 | ```py 417 | # python 3.8 418 | import asyncio 419 | 420 | import await_from_rust 421 | 422 | 423 | async def main(): 424 | # Everything works as if it was coroutine... 425 | result = await await_from_rust.my_coroutine() 426 | print(f"my_coroutine returned with: {result!r}") 427 | 428 | # But note that this won't work: 429 | # asyncio.run(await_from_rust.my_coroutine()) 430 | # because asyncio won't recognise it as a `coroutine` object 431 | 432 | asyncio.run(main()) 433 | ``` 434 | 435 | ### Using Rust's futures as Python awaitables 436 | 437 | TODO... ;) 438 | --------------------------------------------------------------------------------