-
Notifications
You must be signed in to change notification settings - Fork 136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PyObject<'p> and Send #15
Comments
I think the ability to unlock the GIL safely is really important - rust extensions are going to want to do I/O. Maybe the annoyance of passing a Python<'p> to every PyObject method could be mitigated by a stateful macro that automatically passes it as the first argument. Like JS' with. There are Python functions for checking if the current thread already holds the GIL which would help not acquire the GIL again unnecessarily (recursively)..? My one concern is that invisibly locking the GIL in a destructor might lead to unexpected deadlock if the user isn't aware of what's happening, but I think if it is clearly documented that the GIL is held during destruction as well as during method calls it's okay. |
I thought some more about safely unlocking the GIL. There are two semi-workable approaches:
Because both Of course, this approach is unnecessarily restrictive in that it prevents passing other non-Send types like More importantly, this approach is misusing what
This approach only works if there's only a single But without
The downside is that sizeof(Python) increases from 0 bytes to pointer-size. And is no longer free to pass around. Or is there some way to create a struct that allows re-borrowing like Additionally, this approach means we have to prevent the user from creating a new In the case of a callback from python (Rust calls Python calls Rust), we could still synthesize an access token in the callback function: the existing access token is currently inaccessible because it is borrowed by the API call into the python runtime. But if the By the way, the Both approaches should be safe unless rust gets scoped TLS that accepts |
In 1 - is "Because both Python and PyObject are Send, the closure cannot call into the python interpreter while the GIL is unlocked." supposed to be !Send I think? 1 is a clever solution, but I'm not sure about flexibility. What if I want to go grind in that closure, but then in the middle of the grinding I need to take the GIL back again? For example, say I want to log something using the python logging system .. (one of the applications I work on does exactly this). How could this be accomplished..? If we pass a channel, we can get messages out, but they can't be received on the same thread until the closure exits and lock is re-taken, which isn't very nice for realtime logging. Unless you want to spawn another thread just to receive messages .. not great. Does rust support something like I don't think you can solve this by passing a callback into the closure that re-takes the GIL, because that callback would have to be Send too ..? So couldn't contain any PyObjects to use. |
Yes, that was supposed to be The closure can still reacquire the GIL by calling But that still doesn't help with the original issue: create python extension objects that contain references to other objects. Approach 1 doesn't handle that case at all. The next best thing would be a hybrid approach, where both But the hybrid approach would be strictly more complicated than approach 2 alone, and I'd like to get rid of the lifetime on Consider a
Edit: more realistic call syntax; as I don't think we'll be able to make |
Actually in approach 2, the convention should be that the |
I'm super-keen to see this move forward as the proof of concept I'd like to write involves a type that holds references to other Python objects. I think the clearest design is to separate the concern of holding the GIL from PyObject and pass a token around explicitly as you have suggested in 2). I'm not exactly sure why the lock token necessarily has to be mut, and would have thought you could just pass around borrowed references to a 0-size struct. I think it's conceptually the same thing as a MutexGuard, which seems to support that...? But I'm guessing there's a reason I am missing :) |
The Of course, |
What's interesting is that removing the lifetime from When I started with rust-cpython, I originally had a different design: The reference-counting pointer was Going back to this kind of design could help with the hybrid approach between the two options: So I see three ways of going forward: Option
|
After writing all that up, my impression is that ergonomics are important enough to rule out the |
I've started implementing this, and discovered that there is a case were we really need
The iterator needs to hold a copy of the token because it's not available in the |
Since the `Python` token no longer is a part of `PyObject`, lots of methods now require the token as additional argument. This [breaking-change] breaks everything!
I think this requires a motivating example to see which ends up being the best. I suggest wrapping a value data type like a |
Well; there's not much options left to check. Option 2 ( The current code implements choice "Option Python: Copy without PyPtr". We could theoretically still add back By the way, I finally added the |
I'm starting to really like the lifetime-freedom of the current approach. I don't think I'll try to reintroduce
|
When defining a python extension type in rust, it's possible for python code to retain a reference to instances of that type, and later use them on another (python) thread.
Thus, python extension types require that their contents are
Send
. (and also'static
, but we could potentially allow'p
if we make the'p
lifetime invariant and use a higher-order lifetime when acquiring the GIL)The problem:
PyObject
itself is notSend
, so it is not possible to create python extension objects that contain references to other python objects :(So, why is
PyObject
notSend
? The python runtime requires that python objects are only used while the global interpreter lock (GIL) is held. But it's no problem to hold a reference to a python object without having the GIL acquired -- as long as you don't access the python object until re-acquiring the GIL.Currently, a
PyObject<'p>
is a*mut ffi::PyObject
with two invariants:a) The
PyObject
owns one reference to the python object (will callPy_DECREF()
in theDrop
impl)b) The GIL is held for at least the lifetime
'p
. (i.e.PyObject<'p>
contains aPython<'p>
)Python<'p>
represents "the current thread holds the GIL for lifetime'p
", and thus is fundamentally notSend
. We could attempt to remove thePython<'p>
fromPyObject<'p>
. This would require the user to explicitly pass in thePython<'p>
for every operation on thePyObject
. But on the other hand, it means we don't need a lifetime onPyObject
(and transitively,PyResult
etc.), so overall it should be an ergonomics win. It would open up some possibilities for a safe API for temporarily releasing the GIL during a long pure-rust computation (Py_BEGIN_ALLOW_THREADS
).But there's a big issue with this approach: the
Drop
impl. CallingPy_DECREF()
requires holding the GIL, so we wouldn't be able to do that inDrop
-- we'd have to provide an explicitfn release(self, Python)
, and any call toDrop
would leak a reference count.Forgetting to call
release()
would be a serious footgun.This becomes less of an issue with static analysis to find missing
release()
calls: humpty_dumpty might be able to help; and hopefully Rust will get linear types in the future.So, do we adopt this change? When writing python extension in rust, I'd say yes, it's worth it. But for embedding python in a rust program, trading a few lifetime annotations for a serious footgun is not exactly a good trade-off.
Maybe we could make
Drop
(re-)acquire the GIL? Locking the GIL recursively is allowed. That would reduce the footgun from a ref leak to a performance problem: instead of a simple decrement (which is getting inlined into the rust code),Drop
would involve two calls into python library, both of which read from TLS and then do some cheap arithmetic+branching. Nothing too expensive, so this might be a workable approach (considering you can prevent the performance loss by explicitly callingrelease()
).The text was updated successfully, but these errors were encountered: