Skip to main content

rb_sys_build/
bindings.rs

1mod sanitizer;
2mod stable_api;
3
4use crate::cc::Build;
5use crate::utils::is_msvc;
6use crate::{debug_log, RbConfig};
7use quote::ToTokens;
8use stable_api::{categorize_bindings, opaqueify_bindings};
9use std::fs::File;
10use std::io::Write;
11use std::path::{Path, PathBuf};
12use std::{env, error::Error};
13use syn::{Expr, ExprLit, ItemConst, Lit};
14
15const WRAPPER_H_CONTENT: &str = include_str!("bindings/wrapper.h");
16
17/// Generate bindings for the Ruby using bindgen.
18pub fn generate(
19    rbconfig: &RbConfig,
20    static_ruby: bool,
21    cfg_out: &mut File,
22) -> Result<PathBuf, Box<dyn Error>> {
23    let out_dir = PathBuf::from(env::var("OUT_DIR")?);
24
25    let mut clang_args = vec![];
26    if let Some(ruby_include_dir) = rbconfig.get("rubyhdrdir") {
27        clang_args.push(format!("-I{}", ruby_include_dir));
28    }
29    if let Some(ruby_arch_include_dir) = rbconfig.get("rubyarchhdrdir") {
30        clang_args.push(format!("-I{}", ruby_arch_include_dir));
31    }
32
33    clang_args.extend(Build::default_cflags());
34    clang_args.extend(rbconfig.cflags.clone());
35    clang_args.extend(rbconfig.cppflags());
36
37    // On Windows x86_64, we need to handle AVX512 FP16 compatibility issues
38    // Clang 20+ includes types like __m512h that aren't compatible with bindgen
39    if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") {
40        // For MinGW toolchain, disable SSE/AVX only for bindgen
41        // This prevents intrinsics headers from loading but doesn't affect the final binary
42        if !is_msvc() {
43            clang_args.push("-mno-sse".to_string());
44            clang_args.push("-mno-avx".to_string());
45        }
46    }
47
48    debug_log!("INFO: using bindgen with clang args: {:?}", clang_args);
49
50    let mut wrapper_h = WRAPPER_H_CONTENT.to_string();
51
52    if !is_msvc() {
53        wrapper_h.push_str("#ifdef HAVE_RUBY_ATOMIC_H\n");
54        wrapper_h.push_str("#include \"ruby/atomic.h\"\n");
55        wrapper_h.push_str("#endif\n");
56    }
57
58    if rbconfig.have_ruby_header("ruby/io/buffer.h") {
59        clang_args.push("-DHAVE_RUBY_IO_BUFFER_H".to_string());
60    }
61
62    let bindings = default_bindgen(clang_args, rbconfig)
63        .allowlist_file(".*ruby.*")
64        .blocklist_item("ruby_abi_version")
65        .blocklist_function("rb_tr_abi_version")
66        .blocklist_function("^__.*")
67        .blocklist_item("RData")
68        .blocklist_function("rb_tr_rdata")
69        .blocklist_function("rb_tr_rtypeddata")
70        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
71
72    let bindings = if cfg!(feature = "bindgen-rbimpls") {
73        bindings
74    } else {
75        bindings
76            .blocklist_item("^rbimpl_.*")
77            .blocklist_item("^RBIMPL_.*")
78    };
79
80    let bindings = if cfg!(feature = "bindgen-deprecated-types") {
81        bindings
82    } else {
83        bindings.blocklist_item("^_bindgen_ty_9.*")
84    };
85
86    let bindings = opaqueify_bindings(rbconfig, bindings, &mut wrapper_h);
87
88    let mut tokens = {
89        write!(std::io::stderr(), "{}", wrapper_h)?;
90        let bindings = bindings.header_contents("wrapper.h", &wrapper_h);
91        let code_string = bindings.generate()?.to_string();
92        syn::parse_file(&code_string)?
93    };
94
95    let slug = rbconfig.ruby_version_slug();
96    let crate_version = env!("CARGO_PKG_VERSION");
97    let out_path = out_dir.join(format!("bindings-{}-{}.rs", crate_version, slug));
98
99    let code = {
100        sanitizer::ensure_backwards_compatible_encoding_pointers(&mut tokens);
101        clean_docs(rbconfig, &mut tokens);
102
103        if is_msvc() {
104            qualify_symbols_for_msvc(&mut tokens, static_ruby, rbconfig);
105        }
106
107        push_cargo_cfg_from_bindings(&tokens, cfg_out)?;
108        categorize_bindings(&mut tokens);
109        tokens.into_token_stream().to_string()
110    };
111
112    let mut out_file = File::create(&out_path)?;
113    std::io::Write::write_all(&mut out_file, code.as_bytes())?;
114    run_rustfmt(&out_path);
115
116    Ok(out_path)
117}
118
119fn run_rustfmt(path: &Path) {
120    let mut cmd = std::process::Command::new("rustfmt");
121    cmd.stderr(std::process::Stdio::inherit());
122    cmd.stdout(std::process::Stdio::inherit());
123
124    cmd.arg(path);
125
126    if let Err(e) = cmd.status() {
127        debug_log!("WARN: failed to run rustfmt: {}", e);
128    }
129}
130
131fn clean_docs(rbconfig: &RbConfig, syntax: &mut syn::File) {
132    if rbconfig.is_cross_compiling() {
133        return;
134    }
135
136    let ver = rbconfig.ruby_version_slug();
137
138    sanitizer::cleanup_docs(syntax, &ver).unwrap_or_else(|e| {
139        debug_log!("WARN: failed to clean up docs, skipping: {}", e);
140    })
141}
142
143fn default_bindgen(clang_args: Vec<String>, _rbconfig: &RbConfig) -> bindgen::Builder {
144    // Disable layout tests and Debug impl on Windows MinGW due to packed struct layout incompatibilities
145    let is_windows_mingw = cfg!(target_os = "windows") && !is_msvc();
146
147    let enable_layout_tests = !is_windows_mingw && cfg!(feature = "bindgen-layout-tests");
148    let impl_debug = !is_windows_mingw && cfg!(feature = "bindgen-impl-debug");
149
150    let mut bindings = bindgen::Builder::default()
151        .rustified_enum(".*")
152        .no_copy("rb_data_type_struct")
153        .derive_eq(true)
154        .derive_debug(true)
155        .clang_args(clang_args)
156        .layout_tests(enable_layout_tests)
157        .blocklist_item("^__darwin_pthread.*")
158        .blocklist_item("^_opaque_pthread.*")
159        .blocklist_item("^__pthread_.*")
160        .blocklist_item("^pthread_.*")
161        .blocklist_item("^rb_native.*")
162        .blocklist_type("INET_PORT_RESERVATION_INSTANCE")
163        .blocklist_type("PINET_PORT_RESERVATION_INSTANCE")
164        .opaque_type("^__sFILE$")
165        .merge_extern_blocks(true)
166        .generate_comments(true)
167        .size_t_is_usize(env::var("CARGO_FEATURE_BINDGEN_SIZE_T_IS_USIZE").is_ok())
168        .impl_debug(impl_debug)
169        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
170
171    // Make __mingw_ldbl_type_t opaque on Windows MinGW to avoid conflicting packed/align representation
172    if cfg!(target_os = "windows") && !is_msvc() {
173        bindings = bindings.opaque_type("__mingw_ldbl_type_t");
174    }
175
176    if env::var("CARGO_FEATURE_BINDGEN_ENABLE_FUNCTION_ATTRIBUTE_DETECTION").is_ok() {
177        bindings.enable_function_attribute_detection()
178    } else {
179        bindings
180    }
181}
182
183// This is needed because bindgen doesn't support the `__declspec(dllimport)` on
184// global variables. Without it, symbols are not found.
185// See https://stackoverflow.com/a/66182704/2057700
186fn qualify_symbols_for_msvc(tokens: &mut syn::File, is_static: bool, rbconfig: &RbConfig) {
187    let kind = if is_static { "static" } else { "dylib" };
188
189    let name = if is_static {
190        rbconfig.libruby_static_name()
191    } else {
192        rbconfig.libruby_so_name()
193    };
194
195    sanitizer::add_link_ruby_directives(tokens, &name, kind).unwrap_or_else(|e| {
196        debug_log!("WARN: failed to add link directives: {}", e);
197    });
198}
199
200// Add things like `#[cfg(ruby_use_transient_heap = "true")]` to the bindings config
201fn push_cargo_cfg_from_bindings(
202    syntax: &syn::File,
203    cfg_out: &mut File,
204) -> Result<(), Box<dyn Error>> {
205    fn is_defines(line: &str) -> bool {
206        line.starts_with("HAVE_RUBY")
207            || line.starts_with("HAVE_RB")
208            || line.starts_with("USE")
209            || line.starts_with("RUBY_DEBUG")
210            || line.starts_with("RUBY_NDEBUG")
211    }
212
213    for item in syntax.items.iter() {
214        if let syn::Item::Const(item) = item {
215            let conf = ConfValue::new(item);
216            let conf_name = conf.name();
217
218            if is_defines(&conf_name) {
219                let name = conf_name.to_lowercase();
220                let val = conf.value_bool().to_string();
221                println!(
222                    r#"cargo:rustc-check-cfg=cfg(ruby_{}, values("true", "false"))"#,
223                    name
224                );
225                println!("cargo:rustc-cfg=ruby_{}=\"{}\"", name, val);
226                println!("cargo:defines_{}={}", name, val);
227                writeln!(cfg_out, "cargo:defines_{}={}", name, val)?;
228            }
229
230            if conf_name.starts_with("RUBY_ABI_VERSION") {
231                println!("cargo:ruby_abi_version={}", conf.value_string());
232                writeln!(cfg_out, "cargo:ruby_abi_version={}", conf.value_string())?;
233            }
234        }
235    }
236
237    Ok(())
238}
239
240/// An autoconf constant in the bindings
241struct ConfValue<'a> {
242    item: &'a syn::ItemConst,
243}
244
245impl<'a> ConfValue<'a> {
246    pub fn new(item: &'a ItemConst) -> Self {
247        Self { item }
248    }
249
250    pub fn name(&self) -> String {
251        self.item.ident.to_string()
252    }
253
254    pub fn value_string(&self) -> String {
255        match &*self.item.expr {
256            Expr::Lit(ExprLit { lit, .. }) => lit.to_token_stream().to_string(),
257            _ => panic!(
258                "Could not convert HAVE_* constant to string: {:#?}",
259                self.item
260            ),
261        }
262    }
263
264    pub fn value_bool(&self) -> bool {
265        match &*self.item.expr {
266            Expr::Lit(ExprLit {
267                lit: Lit::Int(ref lit),
268                ..
269            }) => lit.base10_parse::<u8>().unwrap_or(1) != 0,
270            Expr::Lit(ExprLit {
271                lit: Lit::Bool(ref lit),
272                ..
273            }) => lit.value,
274            _ => panic!(
275                "Could not convert HAVE_* constant to bool: {:#?}",
276                self.item
277            ),
278        }
279    }
280}