Skip to content
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

Renew #[pyproto] implementation for abi3 branch #1254

Merged
merged 3 commits into from
Oct 27, 2020
Merged

Renew #[pyproto] implementation for abi3 branch #1254

merged 3 commits into from
Oct 27, 2020

Conversation

kngwyu
Copy link
Member

@kngwyu kngwyu commented Oct 20, 2020

So what's the problem?
Currently, on the master, #[pyproto] is implemented via 'table initialization'.
I.e., #[pyproto] generates basically such a function:

#[ctor] 
fn __table_initialization() {
    let mut methods = PyNumberMethods::default();
    methods.set_add::<MyClass>();
    <MyClass as HasProtoRegistory>::add_number_methods(methods);  // I don't remember the function name correctly...
}

I designed this since many protocols required such kinds of tables before.
However, when we are to use ABI3, most protocols are initialized via PyType_Slot and tables are unnecessary.
So I decided to renew the whole design, using Vec<PyType_Slot> instead of tables.
I believe the new implementation is simpler but has one disadvantage: weak typing.
PyType_Slot casts function pointers to *mut c_void so the function type is eliminated, which can lead to bugs. We should carefully check the slot and pfunc of each PyType_Slot are correct.

Now #[pyproto] generates (roughly):

pyo3::inventory::submit!( {
    let mut proto_methods = vec![];
    proto_methods.push(<MyClass as PyNumberSlots>::get_add());
    proto_methods.into()
});

@alex
Copy link
Contributor

alex commented Oct 20, 2020

I only skimmed it, but the design makes sense to me.

@davidhewitt
Copy link
Member

👍 looks like a good idea! I've started reviewing but need more time to finish reading. I have a very busy day today so tomorrow is more likely I'm afraid.

Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay in reviewing. Looks good to me. I have a couple of suggestions.

I believe the new implementation is simpler but has one disadvantage: weak typing.
PyType_Slot casts function pointers to *mut c_void so the function type is eliminated, which can lead to bugs. We should carefully check the slot and pfunc of each PyType_Slot are correct.

How about adding an enum so that when we put the proto methods in the inventory we still have strong types? See my other comment.

Also, I think we should be able to remove the special case for the buffer methods and just use slots for them too.

Comment on lines 154 to 158
tokens.push(quote! { let mut proto_methods = vec![]; });
for getter in proto.slot_getters(method_names) {
let get = syn::Ident::new(getter, Span::call_site());
tokens.push(quote! { proto_methods.push(<#ty as #ext_trait>::#get()); });
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this form, which is easier to read imo:

Suggested change
tokens.push(quote! { let mut proto_methods = vec![]; });
for getter in proto.slot_getters(method_names) {
let get = syn::Ident::new(getter, Span::call_site());
tokens.push(quote! { proto_methods.push(<#ty as #ext_trait>::#get()); });
}
let getters = proto.slot_getters(method_names).map(|getter| {
let get = syn::Ident::new(getter, Span::call_site());
quote!(<#ty as #ext_trait>::#get())
});
tokens.push(quote! { let proto_methods = vec![ $(#getters),* ]; });

Copy link
Member

@davidhewitt davidhewitt Oct 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(and similar for the buffer method tokens just above, but I think that we can probably remove those completely.)

}
fn basic_methods() -> Option<NonNull<PyObjectMethods>> {
None
fn get_type_slots() -> Vec<ffi::PyType_Slot> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of storing ffi::PyType_Slot in this inventory, what about storing an enum:

pub enum PyProtoMethodDef {
    #[cfg(not(Py_LIMITED_API))],
    GetBuffer(ffi::getbufferproc),
    #[cfg(not(Py_LIMITED_API))],
    SetBuffer(ffi::setbufferproc),
    SetItem(ffi::ssizeobjargproc),
    DelItem(ffi::ssizeobjargproc),    
}

This would allow us to keep typing inside the inventory, and avoid the special case for the buffer methods in the macros.

We could do the conversion to ffi::PyType_Slot inside get_type_slots().

Also this would work very nicely with the buffer, where slot definitions do exist as long as it is not limited API. https://github.com/python/cpython/blob/cb115e36e1aba04b90b0ecac6f043e60064ac65b/Include/typeslots.h#L7-L8

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this would work very nicely with the buffer, where slot definitions do exist as long as it is not limited API. https://github.com/python/cpython/blob/cb115e36e1aba04b90b0ecac6f043e60064ac65b/Include/typeslots.h#L7-L8

Nice catch, thanks!

Comment on lines +253 to 258
if let Some(buffer) = T::get_buffer() {
unsafe {
(*(*type_object).tp_as_buffer).bf_getbuffer = buffer.as_ref().bf_getbuffer;
(*(*type_object).tp_as_buffer).bf_releasebuffer = buffer.as_ref().bf_releasebuffer;
(*(*type_object).tp_as_buffer).bf_getbuffer = buffer.bf_getbuffer;
(*(*type_object).tp_as_buffer).bf_releasebuffer = buffer.bf_releasebuffer;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this special case now, because buffer methods do have slot definitions (when not limited abi). See my other comment.

@kngwyu
Copy link
Member Author

kngwyu commented Oct 25, 2020

Thanks for the review!

How about adding an enum so that when we put the proto methods in the inventory we still have strong types?

This sounds the same as the current approach, and if so, it does not address the code complexity issue.
I think what we need is adding some assertion before we cast a function pointer to *mut c_void. I'd rather try to implement this using a macro and ask your review again.

@kngwyu
Copy link
Member Author

kngwyu commented Oct 25, 2020

Oh, I just recalled that type slots does not work for buffer... (#1132 (comment))

Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I just recalled that type slots does not work for buffer... (#1132 (comment))

👍 let's leave it as-is for this PR. I think it might work for 3.9 - I'll experiment in a follow-up PR sometime.

This sounds the same as the current approach, and if so, it does not address the code complexity issue.
I think what we need is adding some assertion before we cast a function pointer to *mut c_void. I'd rather try to implement this using a macro and ask your review again.

Fair point. See my new suggestion in comments below, which I think can reduce LOC quite a bit.

tokens.push(quote! { let mut proto_methods = vec![]; });
for getter in proto.slot_getters(method_names) {
let get = syn::Ident::new(getter, Span::call_site());
tokens.push(quote! { proto_methods.push(<#ty as #ext_trait>::#get()); });
Copy link
Member

@davidhewitt davidhewitt Oct 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can do the creation of PyType_Slot objects and conversion to *mut void here?

Then the slot getters can return well-typed function pointers.

Suggested change
tokens.push(quote! { proto_methods.push(<#ty as #ext_trait>::#get()); });
tokens.push(quote! { proto_methods.push(pyo3::ffi::PyType_Slot {
slot: #slot,
pfunc: <#ty as #ext_trait>::#get() as _
}); });

We would need to move the #slot definitions into the proc macro. I think that could be a good idea anyway to help keep the definitions in one place.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds nice 👍 !

// Set functions used by `#[pyproto]`.
pub fn set_str<T>(&mut self)
pub trait PyBasicSlots {
fn get_str() -> ffi::PyType_Slot
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you take my suggestion above then this should just return the typed function pointer:

Suggested change
fn get_str() -> ffi::PyType_Slot
fn get_str() -> ffi::reprfunc {
py_unary_func!(PyObjectStrProtocol, Self::__str__)
}

And same for all other methods. I think this'll reduce the LOC quite a lot!

@kngwyu
Copy link
Member Author

kngwyu commented Oct 26, 2020

Chaned to use struct TypedSlot<T: Sized>(pub std::os::raw::c_int, pub T) as internal representation of ffi::PyType_Slot to resolve the weak typing problem.

I also tried to slot: #slot that @davidhewitt mentioned, but found myself hard to write "ffi::Py_*"s without code completion.

Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, LGTM!

@kngwyu kngwyu merged commit 95bec25 into abi3 Oct 27, 2020
@kngwyu kngwyu deleted the abi3-new-proto branch October 27, 2020 03:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants