ispc_compile/
lib.rs

1//! A small library meant to be used as a build dependency with Cargo for easily
2//! integrating [ISPC](https://ispc.github.io/) code into Rust projects. The
3//! `ispc_compile` crate provides functionality to use the ISPC compiler
4//! from your build script to build your ISPC code into a library,
5//! and generate Rust bindings to this library. The `ispc_rt` crate is
6//! still required at runtime to provide a macro to import the generated
7//! bindings, along with the task system and performance instrumentation system.
8//!
9//! # Requirements for Compiling ISPC Code
10//!
11//! Both the [ISPC compiler](https://ispc.github.io/) and [libclang](http://clang.llvm.org/)
12//! (for [rust-bindgen](https://github.com/crabtw/rust-bindgen)) must be available in your path
13//! to compile the ISPC code and generate the bindings. These are not required if using `ispc_rt`
14//! to link against a previously compiled library.
15//!
16//! ## Windows Users
17//!
18//! You'll need Visual Studio and will have to use the MSVC ABI version of Rust since ISPC
19//! and Clang link with MSVC on Windows. For bindgen to find libclang you'll need to copy
20//! `libclang.lib` to `clang.lib` and place it in your path.
21//!
22
23pub mod opt;
24
25pub use bindgen;
26
27use std::collections::BTreeSet;
28use std::env;
29use std::fmt::Display;
30use std::fs::File;
31use std::io::{BufRead, BufReader, Write};
32use std::path::{Path, PathBuf};
33use std::process::{Command, ExitStatus};
34
35use regex::Regex;
36use semver::{BuildMetadata, Prerelease, Version};
37
38pub use crate::opt::{
39    Addressing, Architecture, MathLib, OptimizationOpt, TargetISA, TargetOS, CPU,
40};
41
42/// Compile the list of ISPC files into a static library and generate bindings
43/// using bindgen. The library name should not contain a lib prefix or a lib
44/// extension like '.a' or '.lib', the appropriate prefix and suffix will be
45/// added based on the compilation target.
46///
47/// This function will exit the process with `EXIT_FAILURE` if any stage of
48/// compilation or linking fails.
49///
50/// # Example
51/// ```no_run
52/// extern crate ispc_compile;
53///
54/// ispc_compile::compile_library("foo", &["src/foo.ispc", "src/bar.ispc"]);
55/// ```
56pub fn compile_library(lib: &str, files: &[&str]) {
57    let mut cfg = Config::new();
58    for f in files {
59        cfg.file(*f);
60    }
61    cfg.compile(lib)
62}
63
64/// Handy wrapper around calling exit that will log the message passed first
65/// then exit with a failure exit code.
66macro_rules! exit_failure {
67    ($fmt:expr) => {{
68        eprintln!($fmt);
69        std::process::exit(libc::EXIT_FAILURE);
70    }};
71    ($fmt:expr, $($arg:tt)*) => {{
72        eprintln!($fmt, $($arg)*);
73        std::process::exit(libc::EXIT_FAILURE);
74    }}
75}
76
77/// Extra configuration to be passed to ISPC
78pub struct Config {
79    ispc_version: Version,
80    ispc_files: Vec<PathBuf>,
81    include_paths: Vec<PathBuf>,
82    // These options are set from the environment if not set by the user
83    out_dir: Option<PathBuf>,
84    debug: Option<bool>,
85    opt_level: Option<u32>,
86    target: Option<String>,
87    cargo_metadata: bool,
88    // Additional ISPC compiler options that the user can set
89    defines: Vec<(String, Option<String>)>,
90    math_lib: MathLib,
91    addressing: Option<Addressing>,
92    optimization_opts: BTreeSet<OptimizationOpt>,
93    cpu_target: Option<CPU>,
94    force_alignment: Option<u32>,
95    no_omit_frame_ptr: bool,
96    no_stdlib: bool,
97    no_cpp: bool,
98    quiet: bool,
99    werror: bool,
100    woff: bool,
101    wno_perf: bool,
102    instrument: bool,
103    enable_llvm_intrinsics: bool,
104    target_isa: Option<Vec<TargetISA>>,
105    architecture: Option<Architecture>,
106    target_os: Option<TargetOS>,
107    darwin_version_min: Option<(u32, u32)>,
108    bindgen_builder: bindgen::Builder,
109}
110
111impl Config {
112    pub fn new() -> Config {
113        // Query the ISPC compiler version. This also acts as a check that we can
114        // find the ISPC compiler when we need it later.
115        let cmd_output = Command::new("ispc")
116            .arg("--version")
117            .output()
118            .expect("Failed to find ISPC compiler in PATH");
119        if !cmd_output.status.success() {
120            exit_failure!("Failed to get ISPC version, is it in your PATH?");
121        }
122        let ver_string = String::from_utf8_lossy(&cmd_output.stdout);
123        // The ISPC version will be the first version number printed
124        let re = Regex::new(r"(\d+\.\d+\.\d+)").unwrap();
125        let ispc_ver = Version::parse(
126            re.captures_iter(&ver_string)
127                .next()
128                .expect("Failed to parse ISPC version")
129                .get(1)
130                .expect("Failed to parse ISPC version")
131                .as_str(),
132        )
133        .expect("Failed to parse ISPC version");
134
135        Config {
136            ispc_version: ispc_ver,
137            ispc_files: Vec::new(),
138            include_paths: Vec::new(),
139            out_dir: None,
140            debug: None,
141            opt_level: None,
142            target: None,
143            cargo_metadata: true,
144            defines: Vec::new(),
145            math_lib: MathLib::ISPCDefault,
146            addressing: None,
147            optimization_opts: BTreeSet::new(),
148            cpu_target: None,
149            force_alignment: None,
150            no_omit_frame_ptr: false,
151            no_stdlib: false,
152            no_cpp: false,
153            quiet: false,
154            werror: false,
155            woff: false,
156            wno_perf: false,
157            instrument: false,
158            enable_llvm_intrinsics: false,
159            target_isa: None,
160            architecture: None,
161            target_os: None,
162            darwin_version_min: None,
163            bindgen_builder: Default::default(),
164        }
165    }
166    /// Add an ISPC file to be compiled
167    pub fn file<P: AsRef<Path>>(&mut self, file: P) -> &mut Config {
168        self.ispc_files.push(file.as_ref().to_path_buf());
169        self
170    }
171    /// Set the output directory to override the default of `env!("OUT_DIR")`
172    pub fn out_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Config {
173        self.out_dir = Some(dir.as_ref().to_path_buf());
174        self
175    }
176    /// Set whether debug symbols should be generated, symbols are generated by
177    /// default if `env!("DEBUG") == "true"`
178    pub fn debug(&mut self, debug: bool) -> &mut Config {
179        self.debug = Some(debug);
180        self
181    }
182    /// Set the optimization level to override the default of `env!("OPT_LEVEL")`
183    pub fn opt_level(&mut self, opt_level: u32) -> &mut Config {
184        self.opt_level = Some(opt_level);
185        self
186    }
187    /// Set the target triple to compile for, overriding the default of `env!("TARGET")`
188    pub fn target(&mut self, target: &str) -> &mut Config {
189        self.target = Some(target.to_string());
190        self
191    }
192    /// Add a define to be passed to the ISPC compiler, e.g. `-DFOO`
193    /// or `-DBAR=FOO` if a value should also be set.
194    pub fn add_define(&mut self, define: &str, value: Option<&str>) -> &mut Config {
195        self.defines
196            .push((define.to_string(), value.map(|s| s.to_string())));
197        self
198    }
199    /// Select the 32 or 64 bit addressing calculations for addressing calculations in ISPC.
200    pub fn addressing(&mut self, addressing: Addressing) -> &mut Config {
201        self.addressing = Some(addressing);
202        self
203    }
204    /// Set the math library used by ISPC code, defaults to the ISPC math library.
205    pub fn math_lib(&mut self, math_lib: MathLib) -> &mut Config {
206        self.math_lib = math_lib;
207        self
208    }
209    /// Set an optimization option.
210    pub fn optimization_opt(&mut self, opt: OptimizationOpt) -> &mut Config {
211        self.optimization_opts.insert(opt);
212        self
213    }
214    /// Set the cpu target. This overrides the default choice of ISPC which
215    /// is to target the host CPU.
216    pub fn cpu(&mut self, cpu: CPU) -> &mut Config {
217        self.cpu_target = Some(cpu);
218        self
219    }
220    /// Force ISPC memory allocations to be aligned to `alignment`.
221    pub fn force_alignment(&mut self, alignment: u32) -> &mut Config {
222        self.force_alignment = Some(alignment);
223        self
224    }
225    /// Add an extra include path for the ispc compiler to search for files.
226    pub fn include_path<P: AsRef<Path>>(&mut self, path: P) -> &mut Config {
227        self.include_paths.push(path.as_ref().to_path_buf());
228        self
229    }
230    /// Disable frame pointer omission. It may be useful for profiling to
231    /// disable omission.
232    pub fn no_omit_frame_pointer(&mut self) -> &mut Config {
233        self.no_omit_frame_ptr = true;
234        self
235    }
236    /// Don't make the ispc standard library available.
237    pub fn no_stdlib(&mut self) -> &mut Config {
238        self.no_stdlib = true;
239        self
240    }
241    /// Don't run the C preprocessor
242    pub fn no_cpp(&mut self) -> &mut Config {
243        self.no_cpp = true;
244        self
245    }
246    /// Enable suppression of all ispc compiler output.
247    pub fn quiet(&mut self) -> &mut Config {
248        self.quiet = true;
249        self
250    }
251    /// Enable treating warnings as errors.
252    pub fn werror(&mut self) -> &mut Config {
253        self.werror = true;
254        self
255    }
256    /// Disable all warnings.
257    pub fn woff(&mut self) -> &mut Config {
258        self.woff = true;
259        self
260    }
261    /// Don't issue warnings related to performance issues
262    pub fn wno_perf(&mut self) -> &mut Config {
263        self.wno_perf = true;
264        self
265    }
266    /// Emit instrumentation code for ISPC to gather performance data such
267    /// as vector utilization.
268    pub fn instrument(&mut self) -> &mut Config {
269        let min_ver = Version {
270            major: 1,
271            minor: 9,
272            patch: 1,
273            pre: Prerelease::EMPTY,
274            build: BuildMetadata::EMPTY,
275        };
276        if self.ispc_version < min_ver {
277            exit_failure!(
278                "Error: instrumentation is not supported on ISPC versions \
279                          older than 1.9.1 as it generates a non-C compatible header"
280            );
281        }
282        self.instrument = true;
283        self
284    }
285    /// Enable support for LLVM intrinsics
286    pub fn enable_llvm_intrinsics(&mut self) -> &mut Config {
287        self.enable_llvm_intrinsics = true;
288        self
289    }
290    /// Select the target ISA and vector width. If none is specified ispc will
291    /// choose the host CPU ISA and vector width.
292    pub fn target_isa(&mut self, target: TargetISA) -> &mut Config {
293        self.target_isa = Some(vec![target]);
294        self
295    }
296    /// Select multiple target ISAs and vector widths. If none is specified ispc will
297    /// choose the host CPU ISA and vector width.
298    /// Note that certain options are not compatible with this use case,
299    /// e.g. AVX1.1 will replace AVX1, Host should not be passed (just use the default)
300    pub fn target_isas(&mut self, targets: Vec<TargetISA>) -> &mut Config {
301        self.target_isa = Some(targets);
302        self
303    }
304    /// Select the CPU architecture to target
305    pub fn target_arch(&mut self, arch: Architecture) -> &mut Config {
306        self.architecture = Some(arch);
307        self
308    }
309    /// Select the target OS for cross compilation
310    pub fn target_os(&mut self, os: TargetOS) -> &mut Config {
311        self.target_os = Some(os);
312        self
313    }
314    /// Set the minimum macOS/iOS version required for the deployment.
315    pub fn darwin_version_min(&mut self, major: u32, minor: u32) -> &mut Config {
316        self.darwin_version_min = Some((major, minor));
317        self
318    }
319    /// Set whether Cargo metadata should be emitted to link to the compiled library
320    pub fn cargo_metadata(&mut self, metadata: bool) -> &mut Config {
321        self.cargo_metadata = metadata;
322        self
323    }
324    pub fn bindgen_builder(&mut self, builder: bindgen::Builder) -> &mut Self {
325        self.bindgen_builder = builder;
326        self
327    }
328    /// The library name should not have any prefix or suffix, e.g. instead of
329    /// `libexample.a` or `example.lib` simply pass `example`
330    pub fn compile(&self, lib: &str) {
331        let dst = self.get_out_dir();
332        let build_dir = self.get_build_dir();
333        let default_args = self.default_args();
334        dbg!(&default_args);
335        let mut objects = vec![];
336        let mut headers = vec![];
337        for s in &self.ispc_files {
338            let fname = s
339                .file_stem()
340                .expect("ISPC source files must be files")
341                .to_str()
342                .expect("ISPC source file names must be valid UTF-8");
343            self.print(&format!("cargo:rerun-if-changed={}", s.display()));
344
345            let ispc_fname = String::from(fname) + "_ispc";
346            let object = build_dir.join(ispc_fname.clone()).with_extension("o");
347            let header = build_dir.join(ispc_fname.clone()).with_extension("h");
348            let deps = build_dir.join(ispc_fname.clone()).with_extension("idep");
349            let output = Command::new("ispc")
350                .args(&default_args)
351                .arg(s)
352                .arg("-o")
353                .arg(&object)
354                .arg("-h")
355                .arg(&header)
356                .arg("-MMM")
357                .arg(&deps)
358                .output()
359                .unwrap();
360
361            if !output.stderr.is_empty() {
362                let stderr = String::from_utf8_lossy(&output.stderr);
363                for l in stderr.lines() {
364                    self.print(&format!("cargo:warning=(ISPC) {l}"));
365                }
366            }
367            if !output.status.success() {
368                exit_failure!("Failed to compile ISPC source file {}", s.display());
369            }
370            objects.push(object);
371            headers.push(header);
372
373            // Go this files dependencies and add them to Cargo's watch list
374            let deps_list = File::open(deps)
375                .unwrap_or_else(|_| panic!("Failed to open dependencies list for {}", s.display()));
376            let reader = BufReader::new(deps_list);
377            for d in reader.lines() {
378                // Don't depend on the ISPC "stdlib" file which is output as a dependency
379                let dep_name = d.unwrap();
380                self.print(&format!("cargo:rerun-if-changed={dep_name}"));
381            }
382
383            // Push on the additional ISA-specific object files if any were generated
384            if let Some(ref t) = self.target_isa {
385                if t.len() > 1 {
386                    for isa in t.iter() {
387                        let isa_fname = ispc_fname.clone() + "_" + &isa.lib_suffix();
388                        let isa_obj = build_dir.join(isa_fname).with_extension("o");
389                        objects.push(isa_obj);
390                    }
391                }
392            }
393        }
394        let libfile = lib.to_owned() + &self.get_target();
395        if !self.assemble(&libfile, &objects).success() {
396            exit_failure!("Failed to assemble ISPC objects into library {lib}");
397        }
398        self.print(&format!("cargo:rustc-link-lib=static={libfile}"));
399
400        // Now generate a header we can give to bindgen and generate bindings
401        let bindgen_header = self.generate_bindgen_header(lib, &headers);
402        let bindings = self
403            .bindgen_builder
404            .clone()
405            .header(bindgen_header.to_str().unwrap());
406
407        let bindgen_file = dst.join(lib).with_extension("rs");
408
409        let generated_bindings = match bindings.generate() {
410            Ok(b) => b.to_string(),
411            Err(_) => exit_failure!("Failed to generating Rust bindings to {}", lib),
412        };
413        let mut file = match File::create(bindgen_file) {
414            Ok(f) => f,
415            Err(e) => exit_failure!("Failed to open bindgen mod file for writing: {}", e),
416        };
417        file.write_all("#[allow(non_camel_case_types,dead_code,non_upper_case_globals,non_snake_case,improper_ctypes)]\n"
418                       .as_bytes()).unwrap();
419        file.write_all(format!("pub mod {lib} {{\n").as_bytes())
420            .unwrap();
421        file.write_all(generated_bindings.as_bytes()).unwrap();
422        file.write_all(b"}").unwrap();
423
424        self.print(&format!("cargo:rustc-link-search=native={}", dst.display()));
425        self.print(&format!("cargo:rustc-env=ISPC_OUT_DIR={}", dst.display()));
426    }
427    /// Get the ISPC compiler version.
428    pub fn ispc_version(&self) -> &Version {
429        &self.ispc_version
430    }
431    /// Link the ISPC code into a static library on Unix using `ar`
432    #[cfg(unix)]
433    fn assemble(&self, lib: &str, objects: &[PathBuf]) -> ExitStatus {
434        Command::new("ar")
435            .arg("crus")
436            .arg(format!("lib{lib}.a"))
437            .args(objects)
438            .current_dir(self.get_out_dir())
439            .status()
440            .unwrap()
441    }
442    /// Link the ISPC code into a static library on Windows using `lib.exe`
443    #[cfg(windows)]
444    fn assemble(&self, lib: &str, objects: &[PathBuf]) -> ExitStatus {
445        let target = self.get_target();
446        let mut lib_cmd = cc::windows_registry::find_tool(&target, "lib.exe")
447            .expect("Failed to find lib.exe for MSVC toolchain, aborting")
448            .to_command();
449        lib_cmd
450            .arg(format!("/OUT:{lib}.lib"))
451            .args(objects)
452            .current_dir(self.get_out_dir())
453            .status()
454            .unwrap()
455    }
456    /// Generate a single header that includes all of our ISPC headers which we can
457    /// pass to bindgen
458    fn generate_bindgen_header(&self, lib: &str, headers: &[PathBuf]) -> PathBuf {
459        let bindgen_header = self
460            .get_build_dir()
461            .join(format!("_{lib}_ispc_bindgen_header.h"));
462        let mut include_file = File::create(&bindgen_header).unwrap();
463
464        writeln!(include_file, "#include <stdint.h>").unwrap();
465        writeln!(include_file, "#include <stdbool.h>").unwrap();
466
467        for h in headers {
468            writeln!(include_file, "#include \"{}\"", h.display()).unwrap();
469        }
470        bindgen_header
471    }
472    /// Build up list of basic args for each target, debug, opt level, etc.
473    fn default_args(&self) -> Vec<String> {
474        let mut ispc_args = Vec::new();
475        let opt_level = self.get_opt_level();
476        if self.get_debug() {
477            ispc_args.push(String::from("-g"));
478        }
479        if let Some(ref c) = self.cpu_target {
480            ispc_args.push(c.to_string());
481            // The ispc compiler crashes if we give -O0 and --cpu=generic,
482            // see https://github.com/ispc/ispc/issues/1223
483            if *c != CPU::Generic || (*c == CPU::Generic && opt_level != 0) {
484                ispc_args.push(format!("-O{opt_level}"));
485            } else {
486                self.print(
487                    &"cargo:warning=ispc-rs: Omitting -O0 on CPU::Generic target, ispc bug 1223",
488                );
489            }
490        } else {
491            ispc_args.push(format!("-O{opt_level}"));
492        }
493
494        // If we're on Unix we need position independent code
495        if cfg!(unix) {
496            ispc_args.push(String::from("--pic"));
497        }
498        let target = self.get_target();
499        if target.starts_with("i686") {
500            ispc_args.push(String::from("--arch=x86"));
501        } else if target.starts_with("x86_64") {
502            ispc_args.push(String::from("--arch=x86-64"));
503        } else if target.starts_with("aarch64") {
504            ispc_args.push(String::from("--arch=aarch64"));
505        }
506        for (name, value) in &self.defines {
507            match value {
508                Some(value) => ispc_args.push(format!("-D{name}={value}")),
509                None => ispc_args.push(format!("-D{name}")),
510            }
511        }
512        ispc_args.push(self.math_lib.to_string());
513        if let Some(ref s) = self.addressing {
514            ispc_args.push(s.to_string());
515        }
516        if let Some(ref f) = self.force_alignment {
517            ispc_args.push(format!("--force-alignment={f}"));
518        }
519        for o in &self.optimization_opts {
520            ispc_args.push(o.to_string());
521        }
522        for p in &self.include_paths {
523            ispc_args.push(format!("-I{}", p.display()));
524        }
525        if self.no_omit_frame_ptr {
526            ispc_args.push(String::from("--no-omit-frame-pointer"));
527        }
528        if self.no_stdlib {
529            ispc_args.push(String::from("--nostdlib"));
530        }
531        if self.no_cpp {
532            ispc_args.push(String::from("--nocpp"));
533        }
534        if self.quiet {
535            ispc_args.push(String::from("--quiet"));
536        }
537        if self.werror {
538            ispc_args.push(String::from("--werror"));
539        }
540        if self.woff {
541            ispc_args.push(String::from("--woff"));
542        }
543        if self.wno_perf {
544            ispc_args.push(String::from("--wno-perf"));
545        }
546        if self.instrument {
547            ispc_args.push(String::from("--instrument"));
548        }
549        if self.enable_llvm_intrinsics {
550            ispc_args.push(String::from("--enable-llvm-intrinsics"));
551        }
552        if let Some(ref t) = self.target_isa {
553            let mut isa_str = String::from("--target=");
554            for (i, isa) in t.iter().enumerate() {
555                if i > 0 {
556                    isa_str.push(',');
557                }
558                use std::fmt::Write as _;
559                let _ = write!(isa_str, "{isa}");
560            }
561            ispc_args.push(isa_str);
562        } else if target.starts_with("aarch64") {
563            // For arm we may need to override the default target ISA,
564            // e.g. on macOS with ISPC running in Rosetta, ISPC will default to
565            // SSE4, but we need NEON
566
567            ispc_args.push(String::from("--target=neon-i32x4"));
568        }
569        if let Some(ref a) = self.architecture {
570            ispc_args.push(a.to_string());
571        }
572        if let Some(ref o) = self.target_os {
573            ispc_args.push(o.to_string());
574        }
575        if let Some((maj, min)) = self.darwin_version_min {
576            ispc_args.push(format!("--darwin-version-min={maj}.{min}"));
577        }
578        ispc_args
579    }
580    /// Returns the user-set output directory if they've set one, otherwise
581    /// returns env("OUT_DIR")
582    fn get_out_dir(&self) -> PathBuf {
583        let p = self
584            .out_dir
585            .clone()
586            .unwrap_or_else(|| env::var_os("OUT_DIR").map(PathBuf::from).unwrap());
587        if p.is_relative() {
588            env::current_dir().unwrap().join(p)
589        } else {
590            p
591        }
592    }
593    /// Returns the default cargo output dir for build scripts (env("OUT_DIR"))
594    fn get_build_dir(&self) -> PathBuf {
595        env::var_os("OUT_DIR").map(PathBuf::from).unwrap()
596    }
597    /// Returns the user-set debug flag if they've set one, otherwise returns
598    /// env("DEBUG")
599    fn get_debug(&self) -> bool {
600        self.debug
601            .unwrap_or_else(|| env::var("DEBUG").unwrap() == "true")
602    }
603    /// Returns the user-set optimization level if they've set one, otherwise
604    /// returns env("OPT_LEVEL")
605    fn get_opt_level(&self) -> u32 {
606        self.opt_level.unwrap_or_else(|| {
607            let opt = env::var("OPT_LEVEL").unwrap();
608            opt.parse::<u32>().unwrap()
609        })
610    }
611    /// Returns the user-set target triple if they're set one, otherwise
612    /// returns env("TARGET")
613    fn get_target(&self) -> String {
614        self.target
615            .clone()
616            .unwrap_or_else(|| env::var("TARGET").unwrap())
617    }
618    /// Print out cargo metadata if enabled
619    fn print<T: Display>(&self, s: &T) {
620        if self.cargo_metadata {
621            println!("{s}");
622        }
623    }
624}
625
626impl Default for Config {
627    fn default() -> Config {
628        Config::new()
629    }
630}