Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
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
23 changes: 23 additions & 0 deletions .github/workflows/reusable-windows-msi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@1.91.1
with:
targets: i686-pc-windows-msvc,x86_64-pc-windows-msvc,aarch64-pc-windows-msvc
- name: Install LLVM for bindgen
if: inputs.arch != 'arm64'
shell: cmd
run: |
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%"
echo LLVMInstallDir=C:\Program Files\LLVM>> "%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: |
$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
echo "LLVMInstallDir=C:\Program Files\LLVM" >> $env:GITHUB_ENV
- name: Build CPython installer
run: ./Tools/msi/build.bat --doc -"${ARCH}"
shell: bash
23 changes: 23 additions & 0 deletions .github/workflows/reusable-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@1.91.1
Comment thread
Eclips4 marked this conversation as resolved.
Outdated
with:
targets: i686-pc-windows-msvc,x86_64-pc-windows-msvc,aarch64-pc-windows-msvc
- name: Install LLVM for bindgen
if: inputs.arch != 'arm64'
shell: cmd
run: |
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%"
echo LLVMInstallDir=C:\Program Files\LLVM>> "%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: |
$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
echo "LLVMInstallDir=C:\Program Files\LLVM" >> $env:GITHUB_ENV
- 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
136 changes: 130 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 @@ -41,6 +47,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 +67,31 @@ 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 {
matches!(
env::var(name).as_deref(),
Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES")
Comment thread
Eclips4 marked this conversation as resolved.
Outdated
)
}

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 +191,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