Skip to content

Commit ae64e57

Browse files
authored
Introspection: emit messages inline in the binaries instead of introducing a ptr+len indirection (#5450)
* Cleanly fail if the ELF contains too many sections * Introspection: emit messages inline in the binaries instead of introducing a ptr+len indirection Significantly simplifies the parsing I dropped the old code path, we can add it back if we want to maintain backward compatibility * Encode slices as length + value * Fixes expected stubs file
1 parent 5ad0723 commit ae64e57

File tree

7 files changed

+62
-68
lines changed

7 files changed

+62
-68
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,8 +697,7 @@ jobs:
697697
with:
698698
save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }}
699699
- run: python -m pip install --upgrade pip && pip install nox[uv]
700-
# TODO test will be fixed in https://github.com/PyO3/pyo3/pull/5450
701-
# - run: nox -s test-introspection
700+
- run: nox -s test-introspection
702701
env:
703702
CARGO_BUILD_TARGET: ${{ matrix.platform.rust-target }}
704703

newsfragments/5450.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Introspection: change the way introspection data is emitted in the binaries to avoid a pointer indirection and simplify parsing.

pyo3-introspection/src/introspection.rs

Lines changed: 27 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::model::{
22
Argument, Arguments, Attribute, Class, Function, Module, VariableLengthArgument,
33
};
4-
use anyhow::{bail, ensure, Context, Result};
4+
use anyhow::{anyhow, bail, ensure, Context, Result};
5+
use goblin::elf::section_header::SHN_XINDEX;
56
use goblin::elf::Elf;
67
use goblin::mach::load_command::CommandVariant;
78
use goblin::mach::symbols::{NO_SECT, N_SECT};
@@ -11,8 +12,8 @@ use goblin::Object;
1112
use serde::Deserialize;
1213
use std::cmp::Ordering;
1314
use std::collections::HashMap;
14-
use std::fs;
1515
use std::path::Path;
16+
use std::{fs, str};
1617

1718
/// Introspect a cdylib built with PyO3 and returns the definition of a Python module.
1819
///
@@ -268,13 +269,12 @@ fn find_introspection_chunks_in_elf(elf: &Elf<'_>, library_content: &[u8]) -> Re
268269
let mut chunks = Vec::new();
269270
for sym in &elf.syms {
270271
if is_introspection_symbol(elf.strtab.get_at(sym.st_name).unwrap_or_default()) {
272+
ensure!(u32::try_from(sym.st_shndx)? != SHN_XINDEX, "Section names length is greater than SHN_LORESERVE in ELF, this is not supported by PyO3 yet");
271273
let section_header = &elf.section_headers[sym.st_shndx];
272274
let data_offset = sym.st_value + section_header.sh_offset - section_header.sh_addr;
273-
chunks.push(read_symbol_value_with_ptr_and_len(
275+
chunks.push(deserialize_chunk(
274276
&library_content[usize::try_from(data_offset).context("File offset overflow")?..],
275-
0,
276-
library_content,
277-
elf.is_64,
277+
elf.little_endian,
278278
)?);
279279
}
280280
}
@@ -311,73 +311,47 @@ fn find_introspection_chunks_in_macho(
311311
{
312312
let section = &sections[nlist.n_sect - 1]; // Sections are counted from 1
313313
let data_offset = nlist.n_value + u64::from(section.offset) - section.addr;
314-
chunks.push(read_symbol_value_with_ptr_and_len(
314+
chunks.push(deserialize_chunk(
315315
&library_content[usize::try_from(data_offset).context("File offset overflow")?..],
316-
0,
317-
library_content,
318-
macho.is_64,
316+
macho.little_endian,
319317
)?);
320318
}
321319
}
322320
Ok(chunks)
323321
}
324322

325323
fn find_introspection_chunks_in_pe(pe: &PE<'_>, library_content: &[u8]) -> Result<Vec<Chunk>> {
326-
let rdata_data_section = pe
327-
.sections
328-
.iter()
329-
.find(|section| section.name().unwrap_or_default() == ".rdata")
330-
.context("No .rdata section found")?;
331-
let rdata_shift = usize::try_from(pe.image_base).context("image_base overflow")?
332-
+ usize::try_from(rdata_data_section.virtual_address)
333-
.context(".rdata virtual_address overflow")?
334-
- usize::try_from(rdata_data_section.pointer_to_raw_data)
335-
.context(".rdata pointer_to_raw_data overflow")?;
336-
337324
let mut chunks = Vec::new();
338325
for export in &pe.exports {
339326
if is_introspection_symbol(export.name.unwrap_or_default()) {
340-
chunks.push(read_symbol_value_with_ptr_and_len(
327+
chunks.push(deserialize_chunk(
341328
&library_content[export.offset.context("No symbol offset")?..],
342-
rdata_shift,
343-
library_content,
344-
pe.is_64,
329+
true,
345330
)?);
346331
}
347332
}
348333
Ok(chunks)
349334
}
350335

351-
fn read_symbol_value_with_ptr_and_len(
352-
value_slice: &[u8],
353-
shift: usize,
354-
full_library_content: &[u8],
355-
is_64: bool,
336+
fn deserialize_chunk(
337+
content_with_chunk_at_the_beginning: &[u8],
338+
is_little_endian: bool,
356339
) -> Result<Chunk> {
357-
let (ptr, len) = if is_64 {
358-
let (ptr, len) = value_slice[..16].split_at(8);
359-
let ptr = usize::try_from(u64::from_le_bytes(
360-
ptr.try_into().context("Too short symbol value")?,
361-
))
362-
.context("Pointer overflow")?;
363-
let len = usize::try_from(u64::from_le_bytes(
364-
len.try_into().context("Too short symbol value")?,
365-
))
366-
.context("Length overflow")?;
367-
(ptr, len)
340+
let length = content_with_chunk_at_the_beginning
341+
.split_at(4)
342+
.0
343+
.try_into()
344+
.context("The introspection chunk must contain a length")?;
345+
let length = if is_little_endian {
346+
u32::from_le_bytes(length)
368347
} else {
369-
let (ptr, len) = value_slice[..8].split_at(4);
370-
let ptr = usize::try_from(u32::from_le_bytes(
371-
ptr.try_into().context("Too short symbol value")?,
372-
))
373-
.context("Pointer overflow")?;
374-
let len = usize::try_from(u32::from_le_bytes(
375-
len.try_into().context("Too short symbol value")?,
376-
))
377-
.context("Length overflow")?;
378-
(ptr, len)
348+
u32::from_be_bytes(length)
379349
};
380-
let chunk = &full_library_content[ptr - shift..ptr - shift + len];
350+
let chunk = content_with_chunk_at_the_beginning
351+
.get(4..4 + length as usize)
352+
.ok_or_else(|| {
353+
anyhow!("The introspection chunk length {length} is greater that the binary size")
354+
})?;
381355
serde_json::from_slice(chunk).with_context(|| {
382356
format!(
383357
"Failed to parse introspection chunk: '{}'",
@@ -389,7 +363,7 @@ fn read_symbol_value_with_ptr_and_len(
389363
fn is_introspection_symbol(name: &str) -> bool {
390364
name.strip_prefix('_')
391365
.unwrap_or(name)
392-
.starts_with("PYO3_INTROSPECTION_0_")
366+
.starts_with("PYO3_INTROSPECTION_1_")
393367
}
394368

395369
#[derive(Deserialize)]

pyo3-macros-backend/src/introspection.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -353,17 +353,10 @@ impl IntrospectionNode<'_> {
353353
fn emit(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream {
354354
let mut content = ConcatenationBuilder::default();
355355
self.add_to_serialization(&mut content, pyo3_crate_path);
356-
let content = content.into_token_stream(pyo3_crate_path);
357-
358-
let static_name = format_ident!("PYO3_INTROSPECTION_0_{}", unique_element_id());
359-
// #[no_mangle] is required to make sure some linkers like Linux ones do not mangle the section name too.
360-
quote! {
361-
const _: () = {
362-
#[used]
363-
#[no_mangle]
364-
static #static_name: &'static [u8] = #content;
365-
};
366-
}
356+
content.into_static(
357+
pyo3_crate_path,
358+
format_ident!("PYO3_INTROSPECTION_1_{}", unique_element_id()),
359+
)
367360
}
368361

369362
fn add_to_serialization(
@@ -530,6 +523,27 @@ impl ConcatenationBuilder {
530523
}
531524
}
532525
}
526+
527+
fn into_static(self, pyo3_crate_path: &PyO3CratePath, ident: Ident) -> TokenStream {
528+
let mut elements = self.elements;
529+
if !self.current_string.is_empty() {
530+
elements.push(ConcatenationBuilderElement::String(self.current_string));
531+
}
532+
533+
// #[no_mangle] is required to make sure some linkers like Linux ones do not mangle the section name too.
534+
quote! {
535+
const _: () = {
536+
const PIECES: &[&[u8]] = &[#(#elements , )*];
537+
const PIECES_LEN: usize = #pyo3_crate_path::impl_::concat::combined_len(PIECES);
538+
#[used]
539+
#[no_mangle]
540+
static #ident: #pyo3_crate_path::impl_::introspection::SerializedIntrospectionFragment<PIECES_LEN> = #pyo3_crate_path::impl_::introspection::SerializedIntrospectionFragment {
541+
length: PIECES_LEN as u32,
542+
fragment: #pyo3_crate_path::impl_::concat::combine_to_array::<PIECES_LEN>(PIECES)
543+
};
544+
};
545+
}
546+
}
533547
}
534548

535549
enum ConcatenationBuilderElement {

pytests/stubs/pyfunctions.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def many_keyword_arguments(
1919
newt: typing.Any | None = None,
2020
owl: typing.Any | None = None,
2121
penguin: typing.Any | None = None,
22-
) -> typing.Any: ...
22+
) -> None: ...
2323
def none() -> None: ...
2424
def positional_only(a: typing.Any, /, b: typing.Any) -> typing.Any: ...
2525
def simple(

src/impl_/introspection.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,9 @@ impl<'a, T: IntoPyObject<'a>> PyReturnType for T {
1515
impl<T: PyReturnType, E> PyReturnType for Result<T, E> {
1616
const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE;
1717
}
18+
19+
#[repr(C)]
20+
pub struct SerializedIntrospectionFragment<const LEN: usize> {
21+
pub length: u32,
22+
pub fragment: [u8; LEN],
23+
}

tests/test_compile_error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ fn test_compile_errors() {
6969
t.compile_fail("tests/ui/invalid_pymodule_glob.rs");
7070
t.compile_fail("tests/ui/invalid_pymodule_trait.rs");
7171
t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs");
72-
#[cfg(feature = "experimental-async")]
72+
#[cfg(all(feature = "experimental-async", not(feature = "experimental-inspect")))]
7373
#[cfg(any(not(Py_LIMITED_API), Py_3_10))] // to avoid PyFunctionArgument for &str
7474
t.compile_fail("tests/ui/invalid_cancel_handle.rs");
7575
t.pass("tests/ui/pymodule_missing_docs.rs");

0 commit comments

Comments
 (0)