Skip to content

Commit 61a59da

Browse files
committed
Read PE exports directly instead of shelling to dumpbin
dumpbin only exists inside a Visual Studio developer environment, which the CI runner never enters, so the symbol check died with WinError 2 after the example had built and passed its tests. Parse the PE export name table instead. It needs no toolchain, so the check keeps working wherever Python runs, and Windows is the platform this check exists to cover: it is the one that can silently fail to export a symbol. Validated against the win_amd64 wheel built from this branch, read on macOS: 252 exports, of which 219 are ccsp_, matching the 219 declared symbols exactly. Non-PE input is rejected rather than misparsed. Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
1 parent 71cdbef commit 61a59da

1 file changed

Lines changed: 48 additions & 2 deletions

File tree

ci/scripts/check_c_api.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import argparse
1919
import platform
2020
import re
21+
import struct
2122
import subprocess
2223
import sys
2324
import sysconfig
@@ -39,10 +40,55 @@ def declared_symbols() -> set[str]:
3940
return symbols
4041

4142

43+
def pe_exported_symbols(library: Path) -> set[str]:
44+
"""Read the export name table out of a PE image.
45+
46+
dumpbin is the obvious tool but only exists inside a Visual Studio developer environment,
47+
which the CI runner does not enter, so parse the image directly instead.
48+
"""
49+
data = library.read_bytes()
50+
pe = struct.unpack_from("<I", data, 0x3C)[0]
51+
if data[pe : pe + 4] != b"PE\0\0":
52+
raise ValueError(f"{library} is not a PE image")
53+
54+
num_sections = struct.unpack_from("<H", data, pe + 6)[0]
55+
opt_size = struct.unpack_from("<H", data, pe + 20)[0]
56+
opt = pe + 24
57+
magic = struct.unpack_from("<H", data, opt)[0]
58+
# The export directory is the first data directory entry; PE32+ places it 16 bytes later.
59+
export_rva = struct.unpack_from("<I", data, opt + (112 if magic == 0x20B else 96))[0]
60+
if not export_rva:
61+
return set()
62+
63+
sections = []
64+
sec = opt + opt_size
65+
for i in range(num_sections):
66+
off = sec + i * 40
67+
virt_addr, raw_size, raw_ptr = struct.unpack_from("<III", data, off + 12)
68+
sections.append((virt_addr, raw_size, raw_ptr))
69+
70+
def to_offset(rva: int) -> int:
71+
for virt_addr, raw_size, raw_ptr in sections:
72+
if virt_addr <= rva < virt_addr + raw_size:
73+
return raw_ptr + (rva - virt_addr)
74+
raise ValueError(f"RVA {rva:#x} outside every section of {library}")
75+
76+
export = to_offset(export_rva)
77+
num_names = struct.unpack_from("<I", data, export + 24)[0]
78+
names_rva = struct.unpack_from("<I", data, export + 32)[0]
79+
names = to_offset(names_rva)
80+
81+
symbols = set()
82+
for i in range(num_names):
83+
name_rva = struct.unpack_from("<I", data, names + i * 4)[0]
84+
start = to_offset(name_rva)
85+
symbols.add(data[start : data.index(b"\0", start)].decode("ascii"))
86+
return symbols
87+
88+
4289
def exported_symbols(library: Path) -> set[str]:
4390
if platform.system() == "Windows":
44-
out = subprocess.run(["dumpbin", "/EXPORTS", str(library)], capture_output=True, text=True, check=True).stdout
45-
return set(re.findall(r"\b(ccsp_[A-Za-z0-9_]+)\b", out))
91+
return {s for s in pe_exported_symbols(library) if s.startswith("ccsp_")}
4692

4793
out = subprocess.run(
4894
["nm", "-gU" if platform.system() == "Darwin" else "-D", "--defined-only", str(library)]

0 commit comments

Comments
 (0)