Skip to content

Commit 56341cb

Browse files
Icxolumejrsdavidhewitt
authored
emit c-string literals on Rust 1.77 or later (#4269)
* emit c-string literals on Rust 1.77 or later * only clone `PyO3CratePath` instead of the whole `Ctx` Co-authored-by: mejrs <[email protected]> --------- Co-authored-by: mejrs <[email protected]> Co-authored-by: David Hewitt <[email protected]>
1 parent ca82681 commit 56341cb

File tree

9 files changed

+100
-31
lines changed

9 files changed

+100
-31
lines changed

pyo3-build-config/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ pub fn print_feature_cfgs() {
141141
println!("cargo:rustc-cfg=invalid_from_utf8_lint");
142142
}
143143

144+
if rustc_minor_version >= 77 {
145+
println!("cargo:rustc-cfg=c_str_lit");
146+
}
147+
144148
// Actually this is available on 1.78, but we should avoid
145149
// https://github.com/rust-lang/rust/issues/124651 just in case
146150
if rustc_minor_version >= 79 {
@@ -167,6 +171,7 @@ pub fn print_expected_cfgs() {
167171
println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)");
168172
println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)");
169173
println!("cargo:rustc-check-cfg=cfg(diagnostic_namespace)");
174+
println!("cargo:rustc-check-cfg=cfg(c_str_lit)");
170175

171176
// allow `Py_3_*` cfgs from the minimum supported version up to the
172177
// maximum minor version (+1 for development for the next)

pyo3-macros-backend/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev", fe
2020
quote = { version = "1", default-features = false }
2121

2222
[dependencies.syn]
23-
version = "2"
23+
version = "2.0.59" # for `LitCStr`
2424
default-features = false
2525
features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"]
2626

27+
[build-dependencies]
28+
pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev" }
29+
2730
[lints]
2831
workspace = true
2932

pyo3-macros-backend/build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fn main() {
2+
pyo3_build_config::print_expected_cfgs();
3+
pyo3_build_config::print_feature_cfgs();
4+
}

pyo3-macros-backend/src/konst.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use std::borrow::Cow;
2+
use std::ffi::CString;
23

3-
use crate::utils::Ctx;
4+
use crate::utils::{Ctx, LitCStr};
45
use crate::{
56
attributes::{self, get_pyo3_options, take_attributes, NameAttribute},
67
deprecations::Deprecations,
78
};
8-
use proc_macro2::{Ident, TokenStream};
9-
use quote::quote;
9+
use proc_macro2::{Ident, Span};
1010
use syn::{
1111
ext::IdentExt,
1212
parse::{Parse, ParseStream},
@@ -29,10 +29,9 @@ impl ConstSpec<'_> {
2929
}
3030

3131
/// Null-terminated Python name
32-
pub fn null_terminated_python_name(&self, ctx: &Ctx) -> TokenStream {
33-
let Ctx { pyo3_path, .. } = ctx;
32+
pub fn null_terminated_python_name(&self, ctx: &Ctx) -> LitCStr {
3433
let name = self.python_name().to_string();
35-
quote!(#pyo3_path::ffi::c_str!(#name))
34+
LitCStr::new(CString::new(name).unwrap(), Span::call_site(), ctx)
3635
}
3736
}
3837

pyo3-macros-backend/src/method.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use std::borrow::Cow;
2+
use std::ffi::CString;
23
use std::fmt::Display;
34

45
use proc_macro2::{Span, TokenStream};
56
use quote::{format_ident, quote, quote_spanned, ToTokens};
67
use syn::{ext::IdentExt, spanned::Spanned, Ident, Result};
78

89
use crate::deprecations::deprecate_trailing_option_default;
9-
use crate::utils::Ctx;
10+
use crate::utils::{Ctx, LitCStr};
1011
use crate::{
1112
attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue},
1213
deprecations::{Deprecation, Deprecations},
@@ -472,12 +473,10 @@ impl<'a> FnSpec<'a> {
472473
})
473474
}
474475

475-
pub fn null_terminated_python_name(&self, ctx: &Ctx) -> TokenStream {
476-
let Ctx { pyo3_path, .. } = ctx;
477-
let span = self.python_name.span();
478-
let pyo3_path = pyo3_path.to_tokens_spanned(span);
476+
pub fn null_terminated_python_name(&self, ctx: &Ctx) -> LitCStr {
479477
let name = self.python_name.to_string();
480-
quote_spanned!(self.python_name.span() => #pyo3_path::ffi::c_str!(#name))
478+
let name = CString::new(name).unwrap();
479+
LitCStr::new(name, self.python_name.span(), ctx)
481480
}
482481

483482
fn parse_fn_type(

pyo3-macros-backend/src/module.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ use crate::{
77
get_doc,
88
pyclass::PyClassPyO3Option,
99
pyfunction::{impl_wrap_pyfunction, PyFunctionOptions},
10-
utils::Ctx,
10+
utils::{Ctx, LitCStr},
1111
};
12-
use proc_macro2::TokenStream;
12+
use proc_macro2::{Span, TokenStream};
1313
use quote::quote;
14+
use std::ffi::CString;
1415
use syn::{
1516
ext::IdentExt,
1617
parse::{Parse, ParseStream},
@@ -403,10 +404,11 @@ fn module_initialization(name: &syn::Ident, ctx: &Ctx) -> TokenStream {
403404
let Ctx { pyo3_path, .. } = ctx;
404405
let pyinit_symbol = format!("PyInit_{}", name);
405406
let name = name.to_string();
407+
let pyo3_name = LitCStr::new(CString::new(name).unwrap(), Span::call_site(), ctx);
406408

407409
quote! {
408410
#[doc(hidden)]
409-
pub const __PYO3_NAME: &'static ::std::ffi::CStr = #pyo3_path::ffi::c_str!(#name);
411+
pub const __PYO3_NAME: &'static ::std::ffi::CStr = #pyo3_name;
410412

411413
pub(super) struct MakeDef;
412414
#[doc(hidden)]

pyo3-macros-backend/src/pyclass.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use crate::pymethod::{
2222
SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__,
2323
};
2424
use crate::pyversions;
25-
use crate::utils::{self, apply_renaming_rule, PythonDoc};
25+
use crate::utils::{self, apply_renaming_rule, LitCStr, PythonDoc};
2626
use crate::utils::{is_abi3, Ctx};
2727
use crate::PyFunctionOptions;
2828

@@ -2016,7 +2016,7 @@ impl<'a> PyClassImplsBuilder<'a> {
20162016
let Ctx { pyo3_path, .. } = ctx;
20172017
let cls = self.cls;
20182018
let doc = self.doc.as_ref().map_or(
2019-
quote! {#pyo3_path::ffi::c_str!("")},
2019+
LitCStr::empty(ctx).to_token_stream(),
20202020
PythonDoc::to_token_stream,
20212021
);
20222022
let is_basetype = self.attr.options.subclass.is_some();

pyo3-macros-backend/src/pymethod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use std::borrow::Cow;
2+
use std::ffi::CString;
23

34
use crate::attributes::{NameAttribute, RenamingRule};
45
use crate::deprecations::deprecate_trailing_option_default;
56
use crate::method::{CallingConvention, ExtractErrorMode, PyArg};
67
use crate::params::{check_arg_for_gil_refs, impl_regular_arg_param, Holders};
7-
use crate::utils::Ctx;
88
use crate::utils::PythonDoc;
9+
use crate::utils::{Ctx, LitCStr};
910
use crate::{
1011
method::{FnArg, FnSpec, FnType, SelfType},
1112
pyfunction::PyFunctionOptions,
@@ -870,8 +871,7 @@ pub enum PropertyType<'a> {
870871
}
871872

872873
impl PropertyType<'_> {
873-
fn null_terminated_python_name(&self, ctx: &Ctx) -> Result<TokenStream> {
874-
let Ctx { pyo3_path, .. } = ctx;
874+
fn null_terminated_python_name(&self, ctx: &Ctx) -> Result<LitCStr> {
875875
match self {
876876
PropertyType::Descriptor {
877877
field,
@@ -892,7 +892,8 @@ impl PropertyType<'_> {
892892
bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`");
893893
}
894894
};
895-
Ok(quote_spanned!(field.span() => #pyo3_path::ffi::c_str!(#name)))
895+
let name = CString::new(name).unwrap();
896+
Ok(LitCStr::new(name, field.span(), ctx))
896897
}
897898
PropertyType::Function { spec, .. } => Ok(spec.null_terminated_python_name(ctx)),
898899
}

pyo3-macros-backend/src/utils.rs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::attributes::{CrateAttribute, RenamingRule};
22
use proc_macro2::{Span, TokenStream};
33
use quote::{quote, ToTokens};
4+
use std::ffi::CString;
45
use syn::spanned::Spanned;
56
use syn::{punctuated::Punctuated, Token};
67

@@ -67,14 +68,59 @@ pub fn option_type_argument(ty: &syn::Type) -> Option<&syn::Type> {
6768
None
6869
}
6970

71+
// TODO: Replace usage of this by [`syn::LitCStr`] when on MSRV 1.77
72+
#[derive(Clone)]
73+
pub struct LitCStr {
74+
lit: CString,
75+
span: Span,
76+
pyo3_path: PyO3CratePath,
77+
}
78+
79+
impl LitCStr {
80+
pub fn new(lit: CString, span: Span, ctx: &Ctx) -> Self {
81+
Self {
82+
lit,
83+
span,
84+
pyo3_path: ctx.pyo3_path.clone(),
85+
}
86+
}
87+
88+
pub fn empty(ctx: &Ctx) -> Self {
89+
Self {
90+
lit: CString::new("").unwrap(),
91+
span: Span::call_site(),
92+
pyo3_path: ctx.pyo3_path.clone(),
93+
}
94+
}
95+
}
96+
97+
impl quote::ToTokens for LitCStr {
98+
fn to_tokens(&self, tokens: &mut TokenStream) {
99+
if cfg!(c_str_lit) {
100+
syn::LitCStr::new(&self.lit, self.span).to_tokens(tokens);
101+
} else {
102+
let pyo3_path = &self.pyo3_path;
103+
let lit = self.lit.to_str().unwrap();
104+
tokens.extend(quote::quote_spanned!(self.span => #pyo3_path::ffi::c_str!(#lit)));
105+
}
106+
}
107+
}
108+
70109
/// A syntax tree which evaluates to a nul-terminated docstring for Python.
71110
///
72111
/// Typically the tokens will just be that string, but if the original docs included macro
73112
/// expressions then the tokens will be a concat!("...", "\n", "\0") expression of the strings and
74-
/// macro parts.
75-
/// contents such as parse the string contents.
113+
/// macro parts. contents such as parse the string contents.
114+
#[derive(Clone)]
115+
pub struct PythonDoc(PythonDocKind);
116+
76117
#[derive(Clone)]
77-
pub struct PythonDoc(TokenStream);
118+
enum PythonDocKind {
119+
LitCStr(LitCStr),
120+
// There is currently no way to `concat!` c-string literals, we fallback to the `c_str!` macro in
121+
// this case.
122+
Tokens(TokenStream),
123+
}
78124

79125
/// Collects all #[doc = "..."] attributes into a TokenStream evaluating to a null-terminated string.
80126
///
@@ -125,7 +171,7 @@ pub fn get_doc(
125171
}
126172
}
127173

128-
let tokens = if !parts.is_empty() {
174+
if !parts.is_empty() {
129175
// Doc contained macro pieces - return as `concat!` expression
130176
if !current_part.is_empty() {
131177
parts.push(current_part.to_token_stream());
@@ -140,17 +186,26 @@ pub fn get_doc(
140186
syn::token::Comma(Span::call_site()).to_tokens(tokens);
141187
});
142188

143-
tokens
189+
PythonDoc(PythonDocKind::Tokens(
190+
quote!(#pyo3_path::ffi::c_str!(#tokens)),
191+
))
144192
} else {
145193
// Just a string doc - return directly with nul terminator
146-
current_part.to_token_stream()
147-
};
148-
PythonDoc(quote!(#pyo3_path::ffi::c_str!(#tokens)))
194+
let docs = CString::new(current_part).unwrap();
195+
PythonDoc(PythonDocKind::LitCStr(LitCStr::new(
196+
docs,
197+
Span::call_site(),
198+
ctx,
199+
)))
200+
}
149201
}
150202

151203
impl quote::ToTokens for PythonDoc {
152204
fn to_tokens(&self, tokens: &mut TokenStream) {
153-
self.0.to_tokens(tokens)
205+
match &self.0 {
206+
PythonDocKind::LitCStr(lit) => lit.to_tokens(tokens),
207+
PythonDocKind::Tokens(toks) => toks.to_tokens(tokens),
208+
}
154209
}
155210
}
156211

@@ -194,6 +249,7 @@ impl Ctx {
194249
}
195250
}
196251

252+
#[derive(Clone)]
197253
pub enum PyO3CratePath {
198254
Given(syn::Path),
199255
Default,

0 commit comments

Comments
 (0)