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
17pub 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 if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") {
40 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 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 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
183fn 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
200fn 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
240struct 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}