Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/setup-rust-windows/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Set up Rust and LLVM for Windows
description: >-
Install the pinned Rust toolchain with Windows cross-compile targets and
a version of LLVM whose libclang is loadable by bindgen.

inputs:
arch:
description: CPU architecture (x64, Win32, arm64)
required: true

runs:
using: composite
steps:
- uses: dtolnay/rust-toolchain@1.91.1
with:
targets: i686-pc-windows-msvc,x86_64-pc-windows-msvc,aarch64-pc-windows-msvc
# LIBCLANG_PATH must be set explicitly so the vcxproj uses the LLVM we
# install here rather than the VS-bundled LLVM whose clang headers have
# AVX intrinsic bugs. The values are hardcoded literals (no attacker-
# controlled input), so the GITHUB_ENV writes are safe.
- name: Install LLVM for bindgen
if: inputs.arch != 'arm64'
shell: cmd
run: | # zizmor: ignore[github-env]
choco install llvm --allow-downgrade --no-progress --version 21.1.0
if not exist "C:\Program Files\LLVM\bin\libclang.dll" exit /b 1
echo LIBCLANG_PATH=C:\Program Files\LLVM\bin>> "%GITHUB_ENV%"
# Chocolatey's LLVM package only ships x64 binaries, which an ARM64-native
# cargo process cannot load. Install the official ARM64 build directly.
- name: Install LLVM for bindgen (ARM64)
if: inputs.arch == 'arm64'
shell: pwsh
run: | # zizmor: ignore[github-env]
$installer = Join-Path $env:RUNNER_TEMP 'LLVM-21.1.0-woa64.exe'
Invoke-WebRequest 'https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.0/LLVM-21.1.0-woa64.exe' -OutFile $installer
Start-Process -Wait -FilePath $installer -ArgumentList '/S','/D=C:\Program Files\LLVM'
if (!(Test-Path 'C:\Program Files\LLVM\bin\libclang.dll')) { exit 1 }
echo "LIBCLANG_PATH=C:\Program Files\LLVM\bin" >> $env:GITHUB_ENV
1 change: 0 additions & 1 deletion .github/workflows/jit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.11'

# PCbuild downloads LLVM automatically:
- name: Windows
if: runner.os == 'Windows'
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/reusable-windows-msi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ./.github/setup-rust-windows
with:
arch: ${{ inputs.arch }}
- name: Build CPython installer
run: ./Tools/msi/build.bat --doc -"${ARCH}"
shell: bash
3 changes: 3 additions & 0 deletions .github/workflows/reusable-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ./.github/setup-rust-windows
with:
arch: ${{ inputs.arch }}
- name: Register MSVC problem matcher
if: inputs.arch != 'Win32'
run: echo "::add-matcher::.github/problem-matchers/msvc.json"
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/tail-call.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Native Windows (debug)
if: runner.os == 'Windows' && matrix.architecture != 'ARM64'
shell: cmd
Expand Down
137 changes: 131 additions & 6 deletions Modules/cpython-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ fn main() {
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
let builddir = env::var("PYTHON_BUILD_DIR").ok();
emit_rerun_instructions(builddir.as_deref());
if gil_disabled(srcdir, builddir.as_deref()) {
let gil_disabled = gil_disabled(srcdir, builddir.as_deref());
if gil_disabled {
println!("cargo:rustc-cfg=py_gil_disabled");
}
println!("cargo::rustc-check-cfg=cfg(py_gil_disabled)");
generate_c_api_bindings(srcdir, builddir.as_deref(), out_path.as_path());
generate_c_api_bindings(
srcdir,
builddir.as_deref(),
out_path.as_path(),
gil_disabled,
);
}

// Bindgen depends on build-time env and, on iOS, can also inherit the
Expand All @@ -24,6 +30,8 @@ fn emit_rerun_instructions(builddir: Option<&str>) {
for var in [
"IPHONEOS_DEPLOYMENT_TARGET",
"LLVM_TARGET",
"PY_DEBUG",
"PY_GIL_DISABLED",
"PYTHON_BUILD_DIR",
"PY_CC",
"PY_CPPFLAGS",
Expand All @@ -41,6 +49,10 @@ fn emit_rerun_instructions(builddir: Option<&str>) {
}

fn gil_disabled(srcdir: &Path, builddir: Option<&str>) -> bool {
if env_var_is_truthy("PY_GIL_DISABLED") {
return true;
}

let mut candidates = Vec::new();
if let Some(build) = builddir {
candidates.push(PathBuf::from(build));
Expand All @@ -57,12 +69,30 @@ fn gil_disabled(srcdir: &Path, builddir: Option<&str>) -> bool {
false
}

fn generate_c_api_bindings(srcdir: &Path, builddir: Option<&str>, out_path: &Path) {
fn env_var_is_truthy(name: &str) -> bool {
env::var(name)
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
.unwrap_or(false)
}

fn generate_c_api_bindings(
srcdir: &Path,
builddir: Option<&str>,
out_path: &Path,
gil_disabled: bool,
) {
let mut builder = bindgen::Builder::default().header("wrapper.h");

// Suppress all clang warnings (deprecation warnings, etc.)
builder = builder.clang_arg("-w");

if env_var_is_truthy("PY_DEBUG") {
builder = builder.clang_arg("-D_DEBUG");
}
if gil_disabled {
builder = builder.clang_arg("-DPy_GIL_DISABLED=1");
}

// Tell clang the correct target triple for cross-compilation when we have
// an LLVM-specific triple. Otherwise let bindgen translate Cargo's TARGET
// itself (e.g. aarch64-apple-ios-sim -> arm64-apple-ios-simulator).
Expand Down Expand Up @@ -162,10 +192,105 @@ fn generate_c_api_bindings(srcdir: &Path, builddir: Option<&str>, out_path: &Pat
.generate()
.expect("Unable to generate bindings");

let dll_name = python_dll_name(srcdir, env_var_is_truthy("PY_DEBUG"), gil_disabled);
let bindings = patch_windows_imported_pointer_globals(bindings.to_string(), &dll_name);

// Write the bindings to the $OUT_DIR/c_api.rs file.
bindings
.write_to_file(out_path.join("c_api.rs"))
.expect("Couldn't write bindings!");
std::fs::write(out_path.join("c_api.rs"), bindings).expect("Couldn't write bindings!");
}

/// Build the Windows DLL base name: `python{major}{minor}[t][_d]`.
fn python_dll_name(srcdir: &Path, debug: bool, gil_disabled: bool) -> String {
let patchlevel = srcdir.join("Include").join("patchlevel.h");
let contents =
std::fs::read_to_string(&patchlevel).expect("failed to read Include/patchlevel.h");

let major = extract_define_int(&contents, "PY_MAJOR_VERSION");
let minor = extract_define_int(&contents, "PY_MINOR_VERSION");

let mut name = format!("python{major}{minor}");
if gil_disabled {
name.push('t');
}
if debug {
name.push_str("_d");
}
name
}

fn extract_define_int(contents: &str, name: &str) -> u32 {
for line in contents.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("#define")
&& let Some(value) = rest.trim().strip_prefix(name)
&& let Ok(n) = value.trim().parse()
{
return n;
}
}
panic!("could not find #define {name} in patchlevel.h");
}

fn patch_windows_imported_pointer_globals(bindings: String, dll_name: &str) -> String {
// On Windows/MSVC, exported data is imported through a synthetic
// "__imp_<symbol>" pointer in the import address table (IAT). A plain
// `extern { pub static X: *mut T; }` linked via import library fails for
// data symbols because the import library only defines `__imp_X`, not `X`.
//
// Using `#[link_name = "__imp_X"]` links successfully but produces a
// single load — returning the *address* of the variable (the IAT slot
// value) rather than the variable's value. C's `__declspec(dllimport)`
// generates two loads to chase the indirection.
//
// The fix: annotate pointer-valued extern statics with `raw-dylib` on
// Windows so Rust generates the import thunk itself and handles the IAT
// indirection correctly — two loads, matching `__declspec(dllimport)`.
let lines: Vec<_> = bindings.lines().collect();
let mut patched = String::with_capacity(bindings.len());
let mut index = 0;

while index < lines.len() {
if lines[index] == "unsafe extern \"C\" {"
&& lines
.get(index + 1)
.and_then(|l| parse_pointer_static_decl(l))
.is_some()
&& lines.get(index + 2).is_some_and(|l| l.trim() == "}")
{
patched.push_str(&format!(
"#[cfg_attr(windows, link(name = \"{dll_name}\", kind = \"raw-dylib\"))]\n"
));
// Keep the original extern block unchanged.
for i in index..index + 3 {
patched.push_str(lines[i]);
patched.push('\n');
}
index += 3;
continue;
}

patched.push_str(lines[index]);
patched.push('\n');
index += 1;
}

patched
}

fn parse_pointer_static_decl(line: &str) -> Option<(&str, bool, &str)> {
let mut decl = line.trim().strip_prefix("pub static ")?;
let is_mut = decl.starts_with("mut ");
if is_mut {
decl = decl.strip_prefix("mut ")?;
}

let (name, ty) = decl.split_once(':')?;
let ty = ty.trim().strip_suffix(';')?;
if !ty.starts_with('*') {
return None;
}

Some((name.trim(), is_mut, ty))
}

fn add_target_clang_args(
Expand Down
Loading
Loading