-
Notifications
You must be signed in to change notification settings - Fork 792
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
Support for type arguments in pyclass #303
Conversation
Great work, thanks 👍 |
Hmm. As a previous C++ dev, it came as a surprise to me that the static variable in the generic trait impl is in fact shared between all instantiations of all types. I did a bit of googling, but it really does appear like there is no way to create per-generic-instantation statics at this point. In this case, we might have to fall back to lazy allocation of the type object in the **Edit: Mh, that would require either a dep on lazy_static or some pretty unholy unsafe pointer casting trickery (since statics cannot have non-const initializers). |
I now implemented the unsafe fn type_object() -> &'static mut ::pyo3::ffi::PyTypeObject {
use std::collections::hash_map::Entry;
let mut map = ::pyo3::typeob::PY_TYPE_OBJ_MAP.lock().unwrap();
let obj = match map.entry(std::any::TypeId::of::<S<T>>()) {
Entry::Occupied(o) => o.into_mut(),
Entry::Vacant(v) => v.insert(::pyo3::ffi::PyTypeObject_INIT),
};
&mut *(obj as *mut _)
} This way, every variant of the generic has it's own type object. It required adding a dep on Also, generics have to be bound to I'll take a look at the required reworks to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this pull requests! In general I like the approach with the HashMap, just some notes:
lazy_static is fine, that crate is so common I expect it to go into the standard library at some point. For Send for PyTypeObject
I'm not sure, I'll have to give that a second look later.
I think it would be better to have one HashMap for every #[pyclass]
. It should also be documented somewhere, that using a generic causes a hashmap to be used in the background (maybe in the class chapter of the guide).
Eventually we'll also need a test that check that python considers the types of two instances of the same generic instantiation as equal and those of different generic instantiation as different
I'd rather apply simpler approach. |
That would only allow converting that rust object in to a generic python object, not to create a python of the type e.g. |
Ah I didn't think about this case, thanks
I can't imagine concrete use cases... 🤔 Now I think this approach is not bad, but I also think we allow users to override |
Sorry I think it's not problematic. |
Looks like we can't change type object names displayed in python, because they're |
While starting to rework the code in order remove I think I might have a better idea that gets rid of both the hash map and the naming issue at the same time. What if, for generic pyclasses, we require users to pass a parameter Possible syntax: #[pyclass(variants="MyStructU32<u32>,MyStructF32<f32>")]
struct MyStruct<T> where T: 'static {
_a: T,
} Or, using repeated arguments (would probably require some rather substantial reworks in #[pyclass(variant="MyStructU32<u32>", variant="MyStructF32<f32>")]
struct MyStruct<T> where T: 'static {
_a: T,
} Edit: This syntax is possible as well -- I think this is my preferred one: #[pyclass(variants("MyStructU32<u32>", "MyStructF32<f32>"))]
struct MyStruct<T> where T: 'static {
_a: T,
} |
#[doc(hidden)]
pub struct PyTypeObSendable(PyTypeObject)
|
Did you read my previous comment? Using this approach, we'll no longer need a dynamic type object lookup, a As indicated by the title, this is all rather work in progress and I never expected it to be merged as-is. Instead, the PR serves as a means of communicating with you guys and figuring out how to design this. I'll write some decent tests as the final step. |
You mean in proc macro, we generate a wrapper structs like struct MyStructU32(MyStructU32) and implement traits for each one? |
Ah I understand, thanks. |
Use `.split_for_impl()` for better generic argument forwarding. Structs with bounds directly on the type argument now work correctly.
Due to `TypeId::of` requirements, type arguments now have to be `'static`
FnSpec::parse has the side-effect of purging pyo3 attributes from the input, so we can only invoke it once
Alright, the usual end of year / new years madness is over and I found some more time to work in this again. The I implemented Compared to my initial approach using the All in all, I think we're better of with the variant based design. The test file for generic classes gives a general idea on how usage would look like with the current design. I refactored In case you are wondering about https://github.com/athre0z/pyo3/commit/22e4a2225adfecd2754b0252bbe29c41740d1f52: I originally expected that I would have to change substantial amounts of code in this file and started by refactoring out most of the code duplication that made it hard to comprehend ("is the definition for func type X different than type Y" etc) and unified it into a macro. It later turned out that I wouldn't have to change this stuff after all, but I didn't want to revert it because I think it improves code quality anyways. If you want me to extract this commit into a dedicated PR, that's no problem. Currently, I think we're slowly moving towards something mergable with this. Please let me know what you guys think! Edit: If you want me to rebase this on master, just say the word. |
Nothing constructive to say other than this will be a great new feature to have, thank you!! 👍 |
) -> TokenStream { | ||
check_generic(name, sig); | ||
|
||
let doc = utils::get_doc(&meth_attrs, true); | ||
let spec = FnSpec::parse(name, sig, meth_attrs); | ||
|
||
macro_rules! make_py_method_def { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this also be a normal function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but it would require 2 more arguments passed to each invocation of the macro/function (the local macro has access to the locals doc
and name
without passing them explicitly) and wrapping the fourth argument into a lambda. Do you think this is preferable? I don't really have a preference here.
The code looks good in general, though there are some merge conflicts due to the rust 2018 migration and I fear I'll cause some more breakages with #332. I also like that we don't need a hashmap any more. Regarding the syntax of the generics: Do we need to declare all variants in the macro attributes, or would it be possible to have a separate macro to add variants? In both cases, do we need the parentheses around the specific types? (e.g. You should also consider using syn::parse for the macro arguments. It's not used in pyo3 because that part of syn is newer than the macros, but it could make new parsing code much more idiomatic. Rebasing onto master would be good, but given the current churn I think it's better to just merge master. That should also unblock travis. |
This parsing API looks a lot cleaner indeed. However, I don't think that it would make a lot of sense to just use it for the new variants stuff. I'd rather go ahead and refactor the whole Anyways -- I'll start by merging master now. Edit: I originally missed this part of your reply:
Hm yeah, I thought about that as well. I think that could be possible by having |
I looked through the required additional changes needed to allow for defining variants outside of the initial definitions and concluded that only a small fraction of what is currently implemented in this PR would fit the new design. Since this PR has way too many commits with discarded changes and discussion regarding old versions already, I'm going to close it here. Unfortunately, the amount of work and thus time that is still required to bring this to a level where it would be an actually useful addition for the user rather than a fancy way of doing what 5 lines of macros can do is significantly more than I can allocate right now. I'll give a brief (and likely incomplete) list of things that need to be tackled in order for this feature to land, in case anyone wants to pick up where I left:
Anyone who is willing to look into this and is unfamiliar with the code base should likely allocate at least two weeks of more or less full time work. This might sound like a lot, but debugging proc macro code is extremely ugly and the majority of PyO3's macro input parsing & code-gen is undocumented and a little chaotic. Perhaps, at some point, it might be a good idea to rewrite it with generics in mind and split the whole stuff into two phases. One for parsing the inputs, storing all requirements in a nice struct and the second one for generating the actual code. IMHO, the mixture of both makes extending the code a rather unpleasant experience. Perhaps, my previous work here at least gives an idea on how to not design type argument support. :D Feel free to cherry-pick anything you feel is worth keeping! |
I did some experiments on support for generic structs. With generics being compiler time, we obviously can't leave them fully generic. However, we could allow for registering multiple variants of a struct with different type arguments under different names, e.g.:
We'd either need to make the breaking change of adding a name argument to
add_class
or add a second methodadd_generic_class
. Also, this clashes withPyTypeInfo
having aconst NAME
property. Judging from a quick grep trough the project, aside frominitialize_type
, it doesn't appear to be used widely, so it might be an option to just drop it from the trait.I'm sure there are more issues that I didn't think of, yet. Please let me know if you like the general idea and in case you do, I'd be happy to contrib all changes required to make this work!