Skip to main content

apple_bindgen/
builder.rs

1pub use crate::{
2    config::{Config, ConfigMap},
3    sdk::{SdkPath, SdkPathError},
4};
5
6#[derive(Debug)]
7pub struct Builder {
8    framework: String,
9    sdk: SdkPath,
10    target: Option<String>,
11    config: Config,
12}
13
14impl Builder {
15    pub fn new(
16        framework: &str,
17        sdk: impl TryInto<SdkPath, Error = SdkPathError>,
18        config: Config,
19    ) -> Result<Self, SdkPathError> {
20        Ok(Self {
21            framework: framework.to_owned(),
22            sdk: sdk.try_into()?,
23            target: None,
24            config,
25        })
26    }
27
28    pub fn with_builtin_config(
29        framework: &str,
30        sdk: impl TryInto<SdkPath, Error = SdkPathError>,
31    ) -> Result<Self, SdkPathError> {
32        Self::new(
33            framework,
34            sdk,
35            ConfigMap::with_builtin_config().framework_config(framework),
36        )
37    }
38
39    pub fn target(mut self, target: impl AsRef<str>) -> Self {
40        assert!(self.target.is_none());
41        self.target = Some(target.as_ref().to_owned());
42        self
43    }
44
45    pub fn bindgen_builder(&self) -> bindgen::Builder {
46        // Begin building the bindgen params.
47        let mut builder = bindgen::Builder::default();
48
49        let mut clang_args = vec!["-x", "objective-c", "-fblocks", "-fmodules"];
50        let target_arg;
51        if let Some(target) = self.target.as_ref() {
52            target_arg = format!("--target={}", target);
53            clang_args.push(&target_arg);
54        }
55
56        clang_args.extend(&[
57            "-isysroot",
58            self.sdk
59                .path()
60                .to_str()
61                .expect("sdk path is not utf-8 representable"),
62        ]);
63
64        builder = builder
65            .clang_args(&clang_args)
66            .layout_tests(self.config.layout_tests)
67            .formatter(bindgen::Formatter::Prettyplease);
68
69        for opaque_type in &self.config.opaque_types {
70            builder = builder.opaque_type(opaque_type);
71        }
72        for blocklist_item in &self.config.blocklist_items {
73            builder = builder.blocklist_item(blocklist_item);
74        }
75
76        builder = builder.header_contents(
77            &format!("{}.h", self.framework),
78            &format!("@import {};", self.framework),
79        );
80
81        // Only generate bindings for items defined in framework headers, the ObjC runtime,
82        // and MacTypes.h. This excludes irrelevant system types (arm_debug_state32_t, etc.)
83        // from non-framework paths like <mach/>, <sys/>, <arm/>.
84        // allowlist_recursively (default true) ensures types referenced by framework APIs
85        // from system headers (e.g. simd types) are still included.
86        builder = builder
87            .allowlist_file(".*\\.framework/.*")
88            .allowlist_file(".*/usr/include/objc/.*")
89            .allowlist_file(".*/usr/include/os/.*")
90            .allowlist_file(".*/usr/include/MacTypes\\.h");
91
92        builder
93    }
94
95    pub fn generate(&self) -> Result<String, bindgen::BindgenError> {
96        let bindgen_builder = self.bindgen_builder();
97
98        // Generate the bindings.
99        let bindings = bindgen_builder.generate()?;
100
101        // TODO: find the best way to do this post-processing
102        let mut out = bindings.to_string();
103
104        // remove redundant and malformed definitions of `id`
105        out = out.replace("pub type id = *mut objc::runtime::Object", "PUB-TYPE-ID");
106        let re = regex::Regex::new("pub type id = .*;").unwrap();
107        out = re.replace_all(&mut out, "").into_owned();
108        out = out.replace("PUB-TYPE-ID", "pub type id = *mut objc::runtime::Object");
109
110        // Bindgen.toml `replacements`
111        for replacement in &self.config.replacements {
112            let (old, new) = replacement
113                .split_once(" #=># ")
114                .expect("Bindgen.toml is malformed");
115            out = out.replace(old, new);
116        }
117
118        // Fix msg_send! arguments that collide with struct type names.
119        // e.g. `msg_send!(*self, setPDFView: PDFView)` where `PDFView`
120        // resolves to the struct constructor instead of the parameter.
121        out = fix_msg_send_type_collisions(&out);
122
123        // Bindgen.toml `impl_debugs`
124        for ty in &self.config.impl_debugs {
125            if out.contains(ty) {
126                out.push_str(&format!(
127                    r#"
128impl ::std::fmt::Debug for {ty} {{
129    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {{
130        f.debug_struct(stringify!({ty}))
131            .finish()
132    }}
133}}
134                "#
135                ));
136            }
137        }
138
139        Ok(out)
140    }
141}
142
143/// Rename msg_send! arguments and fn parameters that collide with top-level item names.
144///
145/// Bindgen may generate methods where a parameter name matches a `pub struct` or
146/// `pub const` defined in the same output. In `msg_send!` macro expansion, these
147/// names resolve to the struct constructor or constant value instead of the local
148/// parameter. This appends `_` to colliding names in both parameter declarations
149/// and `msg_send!` calls.
150fn fix_msg_send_type_collisions(source: &str) -> String {
151    use std::collections::HashSet;
152
153    let mut shadow_names: HashSet<&str> = HashSet::new();
154    for line in source.lines() {
155        let trimmed = line.trim();
156        let rest = trimmed
157            .strip_prefix("pub struct ")
158            .or_else(|| trimmed.strip_prefix("pub const "));
159        if let Some(rest) = rest {
160            if let Some(name) = rest
161                .split(|c: char| !c.is_alphanumeric() && c != '_')
162                .next()
163            {
164                if !name.is_empty() {
165                    shadow_names.insert(name);
166                }
167            }
168        }
169    }
170    if shadow_names.is_empty() {
171        return source.to_string();
172    }
173
174    let msg_arg_re = regex::Regex::new(r" : (\w+)").unwrap();
175    let comma_param_re = regex::Regex::new(r", (\w+): ").unwrap();
176    let paren_param_re = regex::Regex::new(r"\((\w+): ").unwrap();
177
178    let mut in_trait = false;
179    let mut trait_brace_depth: i32 = 0;
180    let mut in_msg_send = false;
181    let mut msg_send_depth: i32 = 0;
182
183    let mut result = String::with_capacity(source.len());
184    for line in source.lines() {
185        let trimmed = line.trim();
186
187        // Track ObjC trait blocks to restrict parameter renames
188        if !in_trait && trimmed.starts_with("pub trait ") {
189            in_trait = true;
190            trait_brace_depth = 0;
191        }
192        if in_trait {
193            for c in line.chars() {
194                match c {
195                    '{' => trait_brace_depth += 1,
196                    '}' => trait_brace_depth -= 1,
197                    _ => {}
198                }
199            }
200            if trait_brace_depth <= 0 {
201                in_trait = false;
202            }
203        }
204
205        // Track multi-line msg_send! blocks (save state before updating)
206        let is_in_msg_send = in_msg_send;
207        if !in_msg_send && trimmed.contains("msg_send") {
208            in_msg_send = true;
209            msg_send_depth = 0;
210        }
211        if in_msg_send {
212            for c in line.chars() {
213                match c {
214                    '(' => msg_send_depth += 1,
215                    ')' => msg_send_depth -= 1,
216                    _ => {}
217                }
218            }
219            if msg_send_depth <= 0 {
220                in_msg_send = false;
221            }
222        }
223
224        let mut fixed = line.to_string();
225        let mut did_fix = false;
226
227        // msg_send! arguments: ` : name` where name shadows a top-level item
228        // Also handle continuation lines of multi-line msg_send! blocks
229        if fixed.contains("msg_send") || is_in_msg_send {
230            let new_fixed = msg_arg_re.replace_all(&fixed, |caps: &regex::Captures| {
231                let name = caps.get(1).unwrap().as_str();
232                if shadow_names.contains(name) {
233                    format!(" : {}_", name)
234                } else {
235                    caps[0].to_string()
236                }
237            });
238            if new_fixed != fixed {
239                fixed = new_fixed.into_owned();
240                did_fix = true;
241            }
242        }
243
244        // Parameter renames only inside trait blocks (extern/free fn params don't collide)
245        if in_trait {
246            // Indented parameter: `        name: Type` (skip fn-definition lines)
247            if !trimmed.starts_with("unsafe fn ") && fixed.starts_with("        ") {
248                let after_indent = &fixed[8..];
249                if let Some(colon_pos) = after_indent.find(": ") {
250                    let candidate = &after_indent[..colon_pos];
251                    if !candidate.is_empty()
252                        && candidate.chars().all(|c| c.is_alphanumeric() || c == '_')
253                        && shadow_names.contains(candidate)
254                    {
255                        fixed = fixed.replacen(
256                            &format!("        {}: ", candidate),
257                            &format!("        {}_: ", candidate),
258                            1,
259                        );
260                        did_fix = true;
261                    }
262                }
263            }
264
265            // Inline parameter: `, name: Type`
266            {
267                let orig = fixed.clone();
268                let new_fixed = comma_param_re.replace_all(&orig, |caps: &regex::Captures| {
269                    let name = caps.get(1).unwrap().as_str();
270                    if shadow_names.contains(name) {
271                        format!(", {}_: ", name)
272                    } else {
273                        caps[0].to_string()
274                    }
275                });
276                if new_fixed != orig {
277                    fixed = new_fixed.into_owned();
278                    did_fix = true;
279                }
280            }
281
282            // Opening-paren parameter: `(name: Type`
283            {
284                let orig = fixed.clone();
285                let new_fixed = paren_param_re.replace_all(&orig, |caps: &regex::Captures| {
286                    let name = caps.get(1).unwrap().as_str();
287                    if shadow_names.contains(name) {
288                        format!("({}_: ", name)
289                    } else {
290                        caps[0].to_string()
291                    }
292                });
293                if new_fixed != orig {
294                    fixed = new_fixed.into_owned();
295                    did_fix = true;
296                }
297            }
298        }
299
300        if did_fix {
301            result.push_str(&fixed);
302        } else {
303            result.push_str(line);
304        }
305        result.push('\n');
306    }
307    result
308}