From 3a03deea9c72fc02871246a6ff0c748a6812667b Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 11 Feb 2026 14:54:26 +0100 Subject: [PATCH 1/3] Make `intern!` & `py_format!` immortal --- src/fmt.rs | 8 +++++++- src/sync.rs | 23 ++++++++++++++++------- src/types/module.rs | 4 ++-- src/types/string.rs | 12 ++++++++++++ 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/fmt.rs b/src/fmt.rs index 2b5554182f1..5482e6bbeca 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -42,7 +42,13 @@ macro_rules! py_format { static INTERNED: $crate::sync::PyOnceLock<$crate::Py<$crate::types::PyString>> = $crate::sync::PyOnceLock::new(); Ok( INTERNED - .get_or_init($py, || $crate::types::PyString::intern($py, static_string).unbind()) + .get_or_init($py, || { + if let Ok(static_c_string) = ::std::ffi::CString::new(static_string) { + $crate::types::PyString::intern_cstr($py, &static_c_string).unbind() + } else { + $crate::types::PyString::intern($py, static_string).unbind() + } + }) .bind($py) .to_owned() ) diff --git a/src/sync.rs b/src/sync.rs index cb71ccd10f5..2e2bcc256dd 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -13,8 +13,9 @@ use crate::{ internal::state::SuspendAttach, sealed::Sealed, types::{PyAny, PyString}, - Bound, Py, Python, + Borrowed, Bound, Py, Python, }; +use std::ffi::CStr; use std::{ cell::UnsafeCell, marker::PhantomData, @@ -229,27 +230,35 @@ impl Drop for GILOnceCell { #[macro_export] macro_rules! intern { ($py: expr, $text: expr) => {{ - static INTERNED: $crate::sync::Interned = $crate::sync::Interned::new($text); + const _CSTR: &::std::ffi::CStr = { + match ::std::ffi::CStr::from_bytes_with_nul(concat!($text, "\0").as_bytes()) { + ::std::result::Result::Ok(s) => s, + ::std::result::Result::Err(_) => { + ::std::panic!("interned string cannot contain interior null bytes") + } + } + }; + static INTERNED: $crate::sync::Interned = $crate::sync::Interned::new(_CSTR); INTERNED.get($py) }}; } /// Implementation detail for `intern!` macro. #[doc(hidden)] -pub struct Interned(&'static str, PyOnceLock>); +pub struct Interned(&'static CStr, PyOnceLock>); impl Interned { /// Creates an empty holder for an interned `str`. - pub const fn new(value: &'static str) -> Self { + pub const fn new(value: &'static CStr) -> Self { Interned(value, PyOnceLock::new()) } /// Gets or creates the interned `str` value. #[inline] - pub fn get<'py>(&self, py: Python<'py>) -> &Bound<'py, PyString> { + pub fn get<'py>(&self, py: Python<'py>) -> Borrowed<'_, 'py, PyString> { self.1 - .get_or_init(py, || PyString::intern(py, self.0).into()) - .bind(py) + .get_or_init(py, || PyString::intern_cstr(py, self.0).unbind()) + .bind_borrowed(py) } } diff --git a/src/types/module.rs b/src/types/module.rs index 795ec737eeb..639b79cd869 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -551,11 +551,11 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { } } -fn __all__(py: Python<'_>) -> &Bound<'_, PyString> { +fn __all__(py: Python<'_>) -> Borrowed<'static, '_, PyString> { intern!(py, "__all__") } -fn __name__(py: Python<'_>) -> &Bound<'_, PyString> { +fn __name__(py: Python<'_>) -> Borrowed<'static, '_, PyString> { intern!(py, "__name__") } diff --git a/src/types/string.rs b/src/types/string.rs index a2010d3f434..84a04ab56a6 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -202,6 +202,18 @@ impl PyString { } } + /// Intern the given C string. + /// + /// Python may keep a reference to the returned string or make it immortal, preventing it from + /// being garbage collected. + pub fn intern_cstr<'py>(py: Python<'py>, s: &CStr) -> Bound<'py, PyString> { + unsafe { + ffi::PyUnicode_InternFromString(s.as_ptr()) + .assume_owned(py) + .cast_into_unchecked() + } + } + /// Attempts to create a Python string from a Python [bytes-like object]. /// /// The `encoding` and `errors` parameters are optional: From fe83a29cf2cf3169831d48c5bde69ff677af5bc0 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 16 Feb 2026 14:51:43 +0100 Subject: [PATCH 2/3] Fall back to `intern` if not valid `CStr` --- src/sync.rs | 51 ++++++++++++++++++--------------------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/src/sync.rs b/src/sync.rs index 2e2bcc256dd..fe081c3bc15 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -9,13 +9,7 @@ //! interpreter. //! //! This module provides synchronization primitives which are able to synchronize under these conditions. -use crate::{ - internal::state::SuspendAttach, - sealed::Sealed, - types::{PyAny, PyString}, - Borrowed, Bound, Py, Python, -}; -use std::ffi::CStr; +use crate::{internal::state::SuspendAttach, sealed::Sealed, types::PyAny, Bound, Python}; use std::{ cell::UnsafeCell, marker::PhantomData, @@ -230,38 +224,27 @@ impl Drop for GILOnceCell { #[macro_export] macro_rules! intern { ($py: expr, $text: expr) => {{ - const _CSTR: &::std::ffi::CStr = { + const STRING: ::std::result::Result<&::std::ffi::CStr, &str> = { match ::std::ffi::CStr::from_bytes_with_nul(concat!($text, "\0").as_bytes()) { - ::std::result::Result::Ok(s) => s, - ::std::result::Result::Err(_) => { - ::std::panic!("interned string cannot contain interior null bytes") - } + ::std::result::Result::Ok(c_str) => ::std::result::Result::Ok(c_str), + ::std::result::Result::Err(_) => ::std::result::Result::Err($text), } }; - static INTERNED: $crate::sync::Interned = $crate::sync::Interned::new(_CSTR); - INTERNED.get($py) + static INTERNED: $crate::sync::PyOnceLock<$crate::Py<$crate::types::PyString>> = + $crate::sync::PyOnceLock::new(); + INTERNED + .get_or_init($py, || match STRING { + ::std::result::Result::Ok(c_str) => { + $crate::types::PyString::intern_cstr($py, c_str).unbind() + } + ::std::result::Result::Err(string) => { + $crate::types::PyString::intern($py, string).unbind() + } + }) + .bind_borrowed($py) }}; } -/// Implementation detail for `intern!` macro. -#[doc(hidden)] -pub struct Interned(&'static CStr, PyOnceLock>); - -impl Interned { - /// Creates an empty holder for an interned `str`. - pub const fn new(value: &'static CStr) -> Self { - Interned(value, PyOnceLock::new()) - } - - /// Gets or creates the interned `str` value. - #[inline] - pub fn get<'py>(&self, py: Python<'py>) -> Borrowed<'_, 'py, PyString> { - self.1 - .get_or_init(py, || PyString::intern_cstr(py, self.0).unbind()) - .bind_borrowed(py) - } -} - /// Extension trait for [`Once`] to help avoid deadlocking when using a [`Once`] when attached to a /// Python thread. pub trait OnceExt: Sealed { @@ -736,6 +719,8 @@ mod tests { use super::*; use crate::types::{PyAnyMethods, PyDict, PyDictMethods}; + #[cfg(feature = "macros")] + use crate::Py; #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "macros")] use std::sync::atomic::{AtomicBool, Ordering}; From bb3f5703c66ccab70806fd4e1d54b4f3e9e6ee7b Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 16 Feb 2026 15:19:15 +0100 Subject: [PATCH 3/3] fix async functions --- src/impl_/coroutine.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs index 0bb2d341a17..d47a8d6b053 100644 --- a/src/impl_/coroutine.rs +++ b/src/impl_/coroutine.rs @@ -2,13 +2,12 @@ use std::future::Future; use crate::{ coroutine::{cancel::ThrowCallback, Coroutine}, - instance::Bound, types::PyString, - Py, PyAny, PyResult, Python, + Borrowed, Py, PyAny, PyResult, Python, }; pub fn new_coroutine<'py, F>( - name: &Bound<'py, PyString>, + name: Borrowed<'_, 'py, PyString>, qualname_prefix: Option<&'static str>, throw_callback: Option, future: F, @@ -16,7 +15,12 @@ pub fn new_coroutine<'py, F>( where F: Future>> + Send + 'static, { - Coroutine::new(Some(name.clone()), qualname_prefix, throw_callback, future) + Coroutine::new( + Some(name.to_owned()), + qualname_prefix, + throw_callback, + future, + ) } /// Handle which assumes that the coroutine is attached to the thread. Unlike `Python<'_>`, this is `Send`.