@@ -10,11 +10,17 @@ fn main() {
1010 let out_path = PathBuf :: from ( env:: var ( "OUT_DIR" ) . unwrap ( ) ) ;
1111 let builddir = env:: var ( "PYTHON_BUILD_DIR" ) . ok ( ) ;
1212 emit_rerun_instructions ( builddir. as_deref ( ) ) ;
13- if gil_disabled ( srcdir, builddir. as_deref ( ) ) {
13+ let gil_disabled = gil_disabled ( srcdir, builddir. as_deref ( ) ) ;
14+ if gil_disabled {
1415 println ! ( "cargo:rustc-cfg=py_gil_disabled" ) ;
1516 }
1617 println ! ( "cargo::rustc-check-cfg=cfg(py_gil_disabled)" ) ;
17- generate_c_api_bindings ( srcdir, builddir. as_deref ( ) , out_path. as_path ( ) ) ;
18+ generate_c_api_bindings (
19+ srcdir,
20+ builddir. as_deref ( ) ,
21+ out_path. as_path ( ) ,
22+ gil_disabled,
23+ ) ;
1824}
1925
2026// Bindgen depends on build-time env and, on iOS, can also inherit the
@@ -24,6 +30,8 @@ fn emit_rerun_instructions(builddir: Option<&str>) {
2430 for var in [
2531 "IPHONEOS_DEPLOYMENT_TARGET" ,
2632 "LLVM_TARGET" ,
33+ "PY_DEBUG" ,
34+ "PY_GIL_DISABLED" ,
2735 "PYTHON_BUILD_DIR" ,
2836 "PY_CC" ,
2937 "PY_CPPFLAGS" ,
@@ -48,6 +56,13 @@ fn emit_rerun_instructions(builddir: Option<&str>) {
4856/// headers. We pass -resource-dir to bindgen's clang so it picks up those
4957/// headers instead of the broken libclang-18 ones.
5058fn newest_clang_resource_dir ( ) -> Option < PathBuf > {
59+ // On Windows, derive the resource directory from LIBCLANG_PATH so that
60+ // bindgen uses headers matching the libclang.dll it loads, rather than
61+ // picking up a different, incompatible, system LLVM installation
62+ if cfg ! ( windows) {
63+ return clang_resource_dir_from_libclang_path ( ) ;
64+ }
65+
5166 let base = Path :: new ( "/usr/lib" ) ;
5267 let mut best: Option < ( u32 , PathBuf ) > = None ;
5368 for entry in std:: fs:: read_dir ( base) . ok ( ) ?. flatten ( ) {
@@ -68,7 +83,39 @@ fn newest_clang_resource_dir() -> Option<PathBuf> {
6883 best. map ( |( _, p) | p)
6984}
7085
86+ /// Derive the clang resource directory from LIBCLANG_PATH.
87+ ///
88+ /// When LIBCLANG_PATH points to e.g. `...\bin`, the resource directory is
89+ /// at `...\lib\clang\<version>`. We pick the highest version found.
90+ fn clang_resource_dir_from_libclang_path ( ) -> Option < PathBuf > {
91+ let libclang_path = env:: var ( "LIBCLANG_PATH" ) . ok ( ) ?;
92+ let bin_dir = Path :: new ( & libclang_path) ;
93+ // LIBCLANG_PATH typically points to a `bin` directory; the resource
94+ // dir lives under the sibling `lib/clang/<version>`.
95+ let clang_lib_dir = bin_dir. parent ( ) ?. join ( "lib" ) . join ( "clang" ) ;
96+ let mut best: Option < ( u32 , PathBuf ) > = None ;
97+ for entry in std:: fs:: read_dir ( & clang_lib_dir) . ok ( ) ?. flatten ( ) {
98+ let name = entry. file_name ( ) ;
99+ let name = name. to_string_lossy ( ) ;
100+ // Version directories can be just a major number (e.g. "18") or
101+ // a full dotted version (e.g. "18.1.3"). Parse the major part.
102+ if let Ok ( ver) = name. split ( '.' ) . next ( ) . unwrap_or ( "" ) . parse :: < u32 > ( ) {
103+ let resource_dir = entry. path ( ) ;
104+ if resource_dir. join ( "include" ) . is_dir ( )
105+ && best. as_ref ( ) . map_or ( true , |( v, _) | ver > * v)
106+ {
107+ best = Some ( ( ver, resource_dir) ) ;
108+ }
109+ }
110+ }
111+ best. map ( |( _, p) | p)
112+ }
113+
71114fn gil_disabled ( srcdir : & Path , builddir : Option < & str > ) -> bool {
115+ if env_var_is_truthy ( "PY_GIL_DISABLED" ) {
116+ return true ;
117+ }
118+
72119 let mut candidates = Vec :: new ( ) ;
73120 if let Some ( build) = builddir {
74121 candidates. push ( PathBuf :: from ( build) ) ;
@@ -85,12 +132,30 @@ fn gil_disabled(srcdir: &Path, builddir: Option<&str>) -> bool {
85132 false
86133}
87134
88- fn generate_c_api_bindings ( srcdir : & Path , builddir : Option < & str > , out_path : & Path ) {
135+ fn env_var_is_truthy ( name : & str ) -> bool {
136+ env:: var ( name)
137+ . map ( |v| matches ! ( v. to_ascii_lowercase( ) . as_str( ) , "1" | "true" | "yes" ) )
138+ . unwrap_or ( false )
139+ }
140+
141+ fn generate_c_api_bindings (
142+ srcdir : & Path ,
143+ builddir : Option < & str > ,
144+ out_path : & Path ,
145+ gil_disabled : bool ,
146+ ) {
89147 let mut builder = bindgen:: Builder :: default ( ) . header ( "wrapper.h" ) ;
90148
91149 // Suppress all clang warnings (deprecation warnings, etc.)
92150 builder = builder. clang_arg ( "-w" ) ;
93151
152+ if env_var_is_truthy ( "PY_DEBUG" ) {
153+ builder = builder. clang_arg ( "-D_DEBUG" ) ;
154+ }
155+ if gil_disabled {
156+ builder = builder. clang_arg ( "-DPy_GIL_DISABLED=1" ) ;
157+ }
158+
94159 // Use the newest clang resource directory available on the system.
95160 // Bindgen links against whatever libclang it finds (often an older
96161 // system version), but the built-in headers in that version may be
@@ -206,10 +271,105 @@ fn generate_c_api_bindings(srcdir: &Path, builddir: Option<&str>, out_path: &Pat
206271 . generate ( )
207272 . expect ( "Unable to generate bindings" ) ;
208273
274+ let dll_name = python_dll_name ( srcdir, env_var_is_truthy ( "PY_DEBUG" ) , gil_disabled) ;
275+ let bindings = patch_windows_imported_pointer_globals ( bindings. to_string ( ) , & dll_name) ;
276+
209277 // Write the bindings to the $OUT_DIR/c_api.rs file.
210- bindings
211- . write_to_file ( out_path. join ( "c_api.rs" ) )
212- . expect ( "Couldn't write bindings!" ) ;
278+ std:: fs:: write ( out_path. join ( "c_api.rs" ) , bindings) . expect ( "Couldn't write bindings!" ) ;
279+ }
280+
281+ /// Build the Windows DLL base name: `python{major}{minor}[t][_d]`.
282+ fn python_dll_name ( srcdir : & Path , debug : bool , gil_disabled : bool ) -> String {
283+ let patchlevel = srcdir. join ( "Include" ) . join ( "patchlevel.h" ) ;
284+ let contents =
285+ std:: fs:: read_to_string ( & patchlevel) . expect ( "failed to read Include/patchlevel.h" ) ;
286+
287+ let major = extract_define_int ( & contents, "PY_MAJOR_VERSION" ) ;
288+ let minor = extract_define_int ( & contents, "PY_MINOR_VERSION" ) ;
289+
290+ let mut name = format ! ( "python{major}{minor}" ) ;
291+ if gil_disabled {
292+ name. push ( 't' ) ;
293+ }
294+ if debug {
295+ name. push_str ( "_d" ) ;
296+ }
297+ name
298+ }
299+
300+ fn extract_define_int ( contents : & str , name : & str ) -> u32 {
301+ for line in contents. lines ( ) {
302+ let trimmed = line. trim ( ) ;
303+ if let Some ( rest) = trimmed. strip_prefix ( "#define" )
304+ && let Some ( value) = rest. trim ( ) . strip_prefix ( name)
305+ && let Ok ( n) = value. trim ( ) . parse ( )
306+ {
307+ return n;
308+ }
309+ }
310+ panic ! ( "could not find #define {name} in patchlevel.h" ) ;
311+ }
312+
313+ fn patch_windows_imported_pointer_globals ( bindings : String , dll_name : & str ) -> String {
314+ // On Windows/MSVC, exported data is imported through a synthetic
315+ // "__imp_<symbol>" pointer in the import address table (IAT). A plain
316+ // `extern { pub static X: *mut T; }` linked via import library fails for
317+ // data symbols because the import library only defines `__imp_X`, not `X`.
318+ //
319+ // Using `#[link_name = "__imp_X"]` links successfully but produces a
320+ // single load — returning the *address* of the variable (the IAT slot
321+ // value) rather than the variable's value. C's `__declspec(dllimport)`
322+ // generates two loads to chase the indirection.
323+ //
324+ // The fix: annotate pointer-valued extern statics with `raw-dylib` on
325+ // Windows so Rust generates the import thunk itself and handles the IAT
326+ // indirection correctly — two loads, matching `__declspec(dllimport)`.
327+ let lines: Vec < _ > = bindings. lines ( ) . collect ( ) ;
328+ let mut patched = String :: with_capacity ( bindings. len ( ) ) ;
329+ let mut index = 0 ;
330+
331+ while index < lines. len ( ) {
332+ if lines[ index] == "unsafe extern \" C\" {"
333+ && lines
334+ . get ( index + 1 )
335+ . and_then ( |l| parse_pointer_static_decl ( l) )
336+ . is_some ( )
337+ && lines. get ( index + 2 ) . is_some_and ( |l| l. trim ( ) == "}" )
338+ {
339+ patched. push_str ( & format ! (
340+ "#[cfg_attr(windows, link(name = \" {dll_name}\" , kind = \" raw-dylib\" ))]\n "
341+ ) ) ;
342+ // Keep the original extern block unchanged.
343+ for i in index..index + 3 {
344+ patched. push_str ( lines[ i] ) ;
345+ patched. push ( '\n' ) ;
346+ }
347+ index += 3 ;
348+ continue ;
349+ }
350+
351+ patched. push_str ( lines[ index] ) ;
352+ patched. push ( '\n' ) ;
353+ index += 1 ;
354+ }
355+
356+ patched
357+ }
358+
359+ fn parse_pointer_static_decl ( line : & str ) -> Option < ( & str , bool , & str ) > {
360+ let mut decl = line. trim ( ) . strip_prefix ( "pub static " ) ?;
361+ let is_mut = decl. starts_with ( "mut " ) ;
362+ if is_mut {
363+ decl = decl. strip_prefix ( "mut " ) ?;
364+ }
365+
366+ let ( name, ty) = decl. split_once ( ':' ) ?;
367+ let ty = ty. trim ( ) . strip_suffix ( ';' ) ?;
368+ if !ty. starts_with ( '*' ) {
369+ return None ;
370+ }
371+
372+ Some ( ( name. trim ( ) , is_mut, ty) )
213373}
214374
215375fn add_target_clang_args (
0 commit comments