Skip to content

Commit ec5f7db

Browse files
claudealex
authored andcommitted
Reduce macro-generated code size
Slims the code generated by #[pyclass], #[pymethods], and #[pyfunction] without changing runtime behavior: - Fuse return-value conversion into single converter calls (wrap_into_ptr / wrap_into_pyobject) - Fuse required-argument unwrap+extract (extract_required_argument, from_py_with_required) and TryFrom<&Bound<T>>-style receiver extraction (extract_receiver, extract_receiver_trusted) - Fold the receiver cast into the trusted self-extraction unsafe block - Declare argument holders with a single tuple pattern and drop the vestigial per-argument Probe import - Emit PyClassImpl consts only when they differ from new trait defaults - Replace per-class compile-time doc concatenation (DOC) with DOC_PIECES assembled once at type-object creation - Outline type_object_raw into pyclass_type_object_raw helper - Share an empty PyClassItems static; skip empty impl blocks and assertion consts; drop the HasAutomaticFromPyObject deprecation probe - Shorten trampoline shim expansion; compact FunctionDescription::new
1 parent 90d63e8 commit ec5f7db

12 files changed

Lines changed: 458 additions & 197 deletions

File tree

pyo3-macros-backend/src/method.rs

Lines changed: 36 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -435,9 +435,12 @@ impl SelfType {
435435
//
436436
// The trailing `?` exists because if the extraction fails here it represents
437437
// a genuine type error, should not fall back to e.g. `ExtractErrorMode::NotImplemented`.
438+
//
439+
// The cast is inlined (without a redundant nested `unsafe` block, unlike
440+
// the `Checked` path below) to keep the generated code small.
438441
quote! {
439442
unsafe { #pyo3_path::impl_::extract_argument::#method::<#cls>(
440-
#arg,
443+
#pyo3_path::impl_::extract_argument::#cast_fn(#py, #slf),
441444
&mut #holder,
442445
) }?
443446
}
@@ -462,9 +465,9 @@ impl SelfType {
462465
}
463466
SelfType::TryFromBoundRef { span, non_null } => {
464467
let bound_ref = if *non_null {
465-
quote! { unsafe { #pyo3_path::Bound::ref_from_non_null(#py, &#slf) } }
468+
quote! { #pyo3_path::Bound::ref_from_non_null(#py, &#slf) }
466469
} else {
467-
quote! { unsafe { #pyo3_path::Bound::ref_from_ptr(#py, &#slf) } }
470+
quote! { #pyo3_path::Bound::ref_from_ptr(#py, &#slf) }
468471
};
469472
let pyo3_path = pyo3_path.to_tokens_spanned(*span);
470473
let receiver = match self_conversion.0 {
@@ -474,39 +477,26 @@ impl SelfType {
474477
// an instance of the correct type (or a compatible subtype) before
475478
// invoking the slot.
476479
//
477-
// The wrapping `Ok(...?)` here is because an error here should not
478-
// be treated by e.g. `ExtractErrorMode::NotImplemented` as falling
479-
// back to the default, but instead a genuine type error.
480-
quote! {
480+
// Note: cast failures inside the fused helper can only occur on
481+
// PyPy (CPython's slot dispatch contract guarantees the receiver
482+
// type); on PyPy under `ExtractErrorMode::NotImplemented` they now
483+
// fall back to the default like `TryFrom` failures always have,
484+
// instead of raising directly.
485+
quote_spanned! { *span =>
481486
unsafe {
482-
#pyo3_path::PyResult::Ok(
483-
#pyo3_path::impl_::extract_argument::cast_bound_ref_trusted::<#cls>(#bound_ref)?
484-
)
487+
#pyo3_path::impl_::extract_argument::extract_receiver_trusted::<#cls, _>(#bound_ref)
485488
}
486489
}
487490
}
488491
SelfConversionPolicyInner::Checked => {
489492
quote_spanned! { *span =>
490-
#bound_ref.cast::<#cls>()
491-
.map_err(::std::convert::Into::<#pyo3_path::PyErr>::into)
493+
unsafe {
494+
#pyo3_path::impl_::extract_argument::extract_receiver::<#cls, _>(#bound_ref)
495+
}
492496
}
493497
}
494498
};
495-
error_mode.handle_error(
496-
quote_spanned! { *span =>
497-
#receiver
498-
.and_then(
499-
#[allow(
500-
clippy::unnecessary_fallible_conversions,
501-
clippy::useless_conversion,
502-
reason = "anything implementing `TryFrom<&Bound>` is permitted"
503-
)]
504-
|bound| ::std::convert::TryFrom::try_from(bound).map_err(::std::convert::Into::into)
505-
)
506-
507-
},
508-
ctx
509-
)
499+
error_mode.handle_error(receiver, ctx)
510500
}
511501
}
512502
}
@@ -900,7 +890,11 @@ impl<'a> FnSpec<'a> {
900890
(None, None)
901891
};
902892
let args = self_arg.into_iter().chain(args);
903-
let ok_wrap = quotes::ok_wrap(ret_ident.to_token_stream(), ctx);
893+
let return_conversion = quotes::wrap_into_pyobject(
894+
ret_ident.to_token_stream(),
895+
quote!(assume_attached.py()),
896+
ctx,
897+
);
904898
quote! {
905899
{
906900
let coroutine = {
@@ -926,8 +920,7 @@ impl<'a> FnSpec<'a> {
926920
function(#(#args),*)
927921
};
928922
let #ret_ident = future.await;
929-
let #ret_ident = #ok_wrap;
930-
#pyo3_path::impl_::wrap::converter(&#ret_ident).map_into_pyobject(assume_attached.py(), #ret_ident)
923+
#return_conversion
931924
},
932925
)
933926
};
@@ -936,10 +929,7 @@ impl<'a> FnSpec<'a> {
936929
}
937930
} else {
938931
let args = self_arg.into_iter().chain(args);
939-
let return_conversion = quotes::map_result_into_ptr(
940-
quotes::ok_wrap(ret_ident.to_token_stream(), ctx),
941-
ctx,
942-
);
932+
let return_conversion = quotes::wrap_into_ptr(ret_ident.to_token_stream(), ctx);
943933
quote! {
944934
{
945935
#init_holders
@@ -986,8 +976,7 @@ impl<'a> FnSpec<'a> {
986976
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
987977
let function = #rust_name; // Shadow the function name to avoid #3017
988978
#warnings
989-
let result = #call;
990-
result
979+
#call
991980
}
992981
}
993982
}
@@ -1001,8 +990,7 @@ impl<'a> FnSpec<'a> {
1001990
let function = #rust_name; // Shadow the function name to avoid #3017
1002991
#arg_convert
1003992
#warnings
1004-
let result = #call;
1005-
result
993+
#call
1006994
}
1007995
);
1008996
}
@@ -1021,8 +1009,7 @@ impl<'a> FnSpec<'a> {
10211009
let function = #rust_name; // Shadow the function name to avoid #3017
10221010
#arg_convert
10231011
#warnings
1024-
let result = #call;
1025-
result
1012+
#call
10261013
}
10271014
}
10281015
}
@@ -1045,20 +1032,22 @@ impl<'a> FnSpec<'a> {
10451032
FnType::FnStatic => quote! { .flags(#pyo3_path::ffi::METH_STATIC) },
10461033
_ => quote! {},
10471034
};
1048-
let trampoline = match convention {
1049-
CallingConvention::Noargs => Ident::new("noargs", Span::call_site()),
1050-
CallingConvention::Fastcall => {
1051-
Ident::new("maybe_fastcall_cfunction_with_keywords", Span::call_site())
1052-
}
1053-
CallingConvention::Varargs => Ident::new("cfunction_with_keywords", Span::call_site()),
1035+
// Constructor names on `PyMethodDef` and (shorter) trampoline aliases in
1036+
// `impl_::trampoline` - the short aliases keep the generated code small.
1037+
let (constructor, trampoline) = match convention {
1038+
CallingConvention::Noargs => ("noargs", "noargs"),
1039+
CallingConvention::Fastcall => ("maybe_fastcall_cfunction_with_keywords", "fastcall_kw"),
1040+
CallingConvention::Varargs => ("cfunction_with_keywords", "cfunc_kw"),
10541041
};
1042+
let constructor = Ident::new(constructor, Span::call_site());
1043+
let trampoline = Ident::new(trampoline, Span::call_site());
10551044
let doc = if let Some(doc) = doc {
10561045
doc.to_cstr_stream(ctx)?
10571046
} else {
10581047
c"".to_token_stream()
10591048
};
10601049
Ok(quote! {
1061-
#pyo3_path::impl_::pymethods::PyMethodDef::#trampoline(
1050+
#pyo3_path::impl_::pymethods::PyMethodDef::#constructor(
10621051
#python_name,
10631052
#pyo3_path::impl_::trampoline::get_trampoline_function!(#trampoline, #wrapper),
10641053
#doc,

pyo3-macros-backend/src/params.rs

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,18 @@ impl Holders {
2929
pub fn init_holders(&self, ctx: &Ctx) -> TokenStream {
3030
let Ctx { pyo3_path, .. } = ctx;
3131
let holders = &self.holders;
32+
if holders.is_empty() {
33+
return TokenStream::new();
34+
}
35+
// The holders are declared with a single tuple pattern to keep the generated code
36+
// small (this also avoids triggering `clippy::let_unit_value` for the holders which
37+
// are just `()`).
38+
let holders_init = holders
39+
.iter()
40+
.map(|_| quote!(#pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT))
41+
.collect::<Vec<_>>();
3242
quote! {
33-
#[allow(clippy::let_unit_value, reason = "many holders are just `()`")]
34-
#(let mut #holders = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT;)*
43+
let (#(mut #holders,)*) = (#(#holders_init,)*);
3544
}
3645
}
3746
}
@@ -102,10 +111,7 @@ pub fn impl_arg_params(
102111
.map(|(name, default_value)| {
103112
let required = default_value.is_none();
104113
quote! {
105-
#pyo3_path::impl_::extract_argument::KeywordOnlyParameterDescription {
106-
name: #name,
107-
required: #required,
108-
}
114+
#pyo3_path::impl_::extract_argument::KeywordOnlyParameterDescription::new(#name, #required)
109115
}
110116
});
111117

@@ -165,14 +171,15 @@ pub fn impl_arg_params(
165171
// create array of arguments, and then parse
166172
(
167173
quote! {
168-
const DESCRIPTION: #pyo3_path::impl_::extract_argument::FunctionDescription = #pyo3_path::impl_::extract_argument::FunctionDescription {
169-
cls_name: #cls_name,
170-
func_name: stringify!(#python_name),
171-
positional_parameter_names: &[#(#positional_parameter_names),*],
172-
positional_only_parameters: #positional_only_parameters,
173-
required_positional_parameters: #required_positional_parameters,
174-
keyword_only_parameters: &[#(#keyword_only_parameters),*],
175-
};
174+
const DESCRIPTION: #pyo3_path::impl_::extract_argument::FunctionDescription =
175+
#pyo3_path::impl_::extract_argument::FunctionDescription::new(
176+
#cls_name,
177+
stringify!(#python_name),
178+
&[#(#positional_parameter_names),*],
179+
#positional_only_parameters,
180+
#required_positional_parameters,
181+
&[#(#keyword_only_parameters),*],
182+
);
176183
let mut #args_array = [::std::option::Option::None; #num_params];
177184
let (_args, _kwargs) = #extract_expression;
178185
#from_py_with
@@ -247,13 +254,10 @@ pub(crate) fn impl_regular_arg_param(
247254
let pyo3_path = pyo3_path.to_tokens_spanned(arg.ty.span());
248255

249256
// Use this macro inside this function, to ensure that all code generated here is associated
250-
// with the function argument
251-
let use_probe = quote! {
252-
#[allow(unused_imports, reason = "`Probe` trait used on negative case only")]
253-
use #pyo3_path::impl_::pyclass::Probe as _;
254-
};
257+
// with the function argument. The `Probe` trait used by some of the extraction machinery is
258+
// imported once per wrapper function (see `Holders::init_holders`).
255259
macro_rules! quote_arg_span {
256-
($($tokens:tt)*) => { quote_spanned!(arg.ty.span() => { #use_probe $($tokens)* }) }
260+
($($tokens:tt)*) => { quote_spanned!(arg.ty.span() => $($tokens)*) }
257261
}
258262

259263
let name_str = arg.name.to_string();
@@ -282,13 +286,14 @@ pub(crate) fn impl_regular_arg_param(
282286
)?
283287
}
284288
} else {
285-
let unwrap = quote! {unsafe { #pyo3_path::impl_::extract_argument::unwrap_required_argument_bound(#arg_value.as_deref()) }};
286289
quote_arg_span! {
287-
#pyo3_path::impl_::extract_argument::from_py_with(
288-
#unwrap,
289-
#name_str,
290-
#extractor,
291-
)?
290+
unsafe {
291+
#pyo3_path::impl_::extract_argument::from_py_with_required(
292+
#arg_value.as_deref(),
293+
#name_str,
294+
#extractor,
295+
)
296+
}?
292297
}
293298
}
294299
} else if let Some(default) = default {
@@ -306,13 +311,14 @@ pub(crate) fn impl_regular_arg_param(
306311
}
307312
} else {
308313
let holder = holders.push_holder(arg.ty.span());
309-
let unwrap = quote! { unsafe { #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value) } };
310314
quote_arg_span! {
311-
#pyo3_path::impl_::extract_argument::extract_argument(
312-
#unwrap,
313-
&mut #holder,
314-
#name_str
315-
)?
315+
unsafe {
316+
#pyo3_path::impl_::extract_argument::extract_required_argument(
317+
#arg_value,
318+
&mut #holder,
319+
#name_str
320+
)
321+
}?
316322
}
317323
}
318324
}

0 commit comments

Comments
 (0)