Skip to main content

wasmer_compiler_llvm/
config.rs

1use crate::compiler::LLVMCompiler;
2use enum_iterator::Sequence;
3pub use inkwell::OptimizationLevel as LLVMOptLevel;
4use inkwell::targets::{
5    CodeModel, InitializationConfig, RelocMode, Target as InkwellTarget, TargetMachine,
6    TargetMachineOptions, TargetTriple,
7};
8use itertools::Itertools;
9use std::fs::File;
10use std::io::{self, Write};
11use std::path::PathBuf;
12use std::sync::Arc;
13use std::{fmt::Debug, num::NonZero};
14use target_lexicon::BinaryFormat;
15use wasmer_compiler::misc::{CompiledKind, function_kind_to_filename};
16use wasmer_compiler::{
17    Compiler, CompilerConfig, DEFAULT_MAX_TABLE_ELEMENTS, Debugger, Engine, EngineBuilder,
18    ModuleMiddleware,
19};
20use wasmer_types::{
21    Features,
22    target::{Architecture, OperatingSystem, Target, Triple},
23};
24
25/// The InkWell ModuleInfo type
26pub type InkwellModule<'ctx> = inkwell::module::Module<'ctx>;
27
28/// The InkWell MemoryBuffer type
29pub type InkwellMemoryBuffer<'a> = inkwell::memory_buffer::MemoryBuffer<'a>;
30
31/// Callbacks to the different LLVM compilation phases.
32#[derive(Debug, Clone)]
33pub struct LLVMCallbacks {
34    debug_dir: PathBuf,
35}
36
37impl LLVMCallbacks {
38    pub fn new(debug_dir: PathBuf) -> Result<Self, io::Error> {
39        // Create the debug dir in case it doesn't exist
40        std::fs::create_dir_all(&debug_dir)?;
41        Ok(Self { debug_dir })
42    }
43
44    /// Returns the debug directory used to dump compilation artifacts.
45    pub fn debug_dir(&self) -> &PathBuf {
46        &self.debug_dir
47    }
48
49    fn base_path(&self, module_hash: &Option<String>) -> PathBuf {
50        let mut path = self.debug_dir.clone();
51        if let Some(hash) = module_hash {
52            path.push(hash);
53        }
54        std::fs::create_dir_all(&path)
55            .unwrap_or_else(|_| panic!("cannot create debug directory: {}", path.display()));
56        path
57    }
58
59    pub fn preopt_ir(
60        &self,
61        kind: &CompiledKind,
62        module_hash: &Option<String>,
63        module: &InkwellModule,
64    ) {
65        let mut path = self.base_path(module_hash);
66        path.push(function_kind_to_filename(kind, ".preopt.ll"));
67        module
68            .print_to_file(&path)
69            .expect("Error while dumping pre optimized LLVM IR");
70    }
71    pub fn postopt_ir(
72        &self,
73        kind: &CompiledKind,
74        module_hash: &Option<String>,
75        module: &InkwellModule,
76    ) {
77        let mut path = self.base_path(module_hash);
78        path.push(function_kind_to_filename(kind, ".postopt.ll"));
79        module
80            .print_to_file(&path)
81            .expect("Error while dumping post optimized LLVM IR");
82    }
83    pub fn obj_memory_buffer(
84        &self,
85        kind: &CompiledKind,
86        module_hash: &Option<String>,
87        memory_buffer: &InkwellMemoryBuffer,
88    ) {
89        let mut path = self.base_path(module_hash);
90        path.push(function_kind_to_filename(kind, ".o"));
91        let mem_buf_slice = memory_buffer.as_slice();
92        let mut file =
93            File::create(path).expect("Error while creating debug object file from LLVM IR");
94        file.write_all(mem_buf_slice).unwrap();
95    }
96
97    pub fn asm_memory_buffer(
98        &self,
99        kind: &CompiledKind,
100        module_hash: &Option<String>,
101        asm_memory_buffer: &InkwellMemoryBuffer,
102    ) {
103        let mut path = self.base_path(module_hash);
104        path.push(function_kind_to_filename(kind, ".s"));
105        let mem_buf_slice = asm_memory_buffer.as_slice();
106        let mut file =
107            File::create(path).expect("Error while creating debug assembly file from LLVM IR");
108        file.write_all(mem_buf_slice).unwrap();
109    }
110}
111
112#[derive(Debug, Clone)]
113pub struct LLVM {
114    pub(crate) enable_nan_canonicalization: bool,
115    pub(crate) enable_non_volatile_memops: bool,
116    pub(crate) enable_readonly_funcref_table: bool,
117    pub(crate) enable_m0: bool,
118    pub(crate) enable_perfmap: bool,
119    pub(crate) debugger: Option<Debugger>,
120    pub(crate) opt_level: LLVMOptLevel,
121    pub(crate) is_pic: bool,
122    pub(crate) experimental_artifact: bool,
123    pub(crate) max_table_elements: u32,
124    pub(crate) callbacks: Option<LLVMCallbacks>,
125    /// The middleware chain.
126    pub(crate) middlewares: Vec<Arc<dyn ModuleMiddleware>>,
127    /// Number of threads to use when compiling a module.
128    pub(crate) num_threads: NonZero<usize>,
129    pub(crate) verbose_asm: bool,
130}
131
132#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Sequence)]
133pub(crate) enum OptimizationStyle {
134    ForSpeed,
135    ForSize,
136    Disabled,
137}
138
139impl LLVM {
140    /// Creates a new configuration object with the default configuration
141    /// specified.
142    pub fn new() -> Self {
143        Self {
144            enable_nan_canonicalization: false,
145            enable_non_volatile_memops: false,
146            enable_readonly_funcref_table: false,
147            enable_m0: true,
148            enable_perfmap: false,
149            debugger: None,
150            opt_level: LLVMOptLevel::Aggressive,
151            is_pic: false,
152            experimental_artifact: false,
153            max_table_elements: DEFAULT_MAX_TABLE_ELEMENTS,
154            callbacks: None,
155            middlewares: vec![],
156            verbose_asm: false,
157            num_threads: std::thread::available_parallelism().unwrap_or(NonZero::new(1).unwrap()),
158        }
159    }
160
161    /// Enable the experimental artifact format.
162    pub fn experimental_artifact(&mut self, enable: bool) -> &mut Self {
163        self.experimental_artifact = enable;
164        // We will link a shared library and so PIC must be enabled.
165        self.is_pic = enable;
166        self
167    }
168
169    /// Set the maximum total number of elements allowed in local fixed-size tables.
170    pub fn max_table_elements(&mut self, max_table_elements: u32) -> &mut Self {
171        self.max_table_elements = max_table_elements;
172        self
173    }
174
175    /// The optimization levels when optimizing the IR.
176    pub fn opt_level(&mut self, opt_level: LLVMOptLevel) -> &mut Self {
177        self.opt_level = opt_level;
178        self
179    }
180
181    pub fn num_threads(&mut self, num_threads: NonZero<usize>) -> &mut Self {
182        self.num_threads = num_threads;
183        self
184    }
185
186    pub fn verbose_asm(&mut self, verbose_asm: bool) -> &mut Self {
187        self.verbose_asm = verbose_asm;
188        self
189    }
190
191    /// Compiler IR verification is always enabled for LLVM.
192    #[deprecated(note = "LLVM compiler IR verification is always enabled")]
193    pub fn enable_verifier(&mut self) {}
194
195    /// Callbacks that will triggered in the different compilation
196    /// phases in LLVM.
197    pub fn callbacks(&mut self, callbacks: Option<LLVMCallbacks>) -> &mut Self {
198        self.callbacks = callbacks;
199        self
200    }
201
202    /// For the LLVM compiler, we can use non-volatile memory operations which lead to a better performance
203    /// (but are not 100% SPEC compliant).
204    pub fn non_volatile_memops(&mut self, enable_non_volatile_memops: bool) -> &mut Self {
205        self.enable_non_volatile_memops = enable_non_volatile_memops;
206        self
207    }
208
209    /// Enables treating eligible funcref tables as read-only so the backend can
210    /// place them in read-only data.
211    pub fn readonly_funcref_table(&mut self, enable_readonly_funcref_table: bool) -> &mut Self {
212        self.enable_readonly_funcref_table = enable_readonly_funcref_table;
213        self
214    }
215
216    fn reloc_mode(&self, binary_format: BinaryFormat) -> RelocMode {
217        if matches!(binary_format, BinaryFormat::Macho) {
218            return RelocMode::Static;
219        }
220
221        if self.is_pic {
222            RelocMode::PIC
223        } else {
224            RelocMode::Static
225        }
226    }
227
228    fn code_model(&self, binary_format: BinaryFormat) -> CodeModel {
229        // We normally use the large code model, but when targeting shared
230        // objects, we are required to use PIC. If we use PIC anyways, we lose
231        // any benefit from large code model and there's some cost on all
232        // platforms, plus some platforms (MachO) don't support PIC + large
233        // at all.
234        if matches!(binary_format, BinaryFormat::Macho) {
235            return CodeModel::Default;
236        }
237
238        if self.is_pic {
239            CodeModel::Small
240        } else {
241            CodeModel::Large
242        }
243    }
244
245    pub(crate) fn target_operating_system(&self, target: &Target) -> OperatingSystem {
246        match target.triple().operating_system {
247            OperatingSystem::Darwin(deployment) if !self.is_pic => {
248                // LLVM detects static relocation + darwin + 64-bit and
249                // force-enables PIC because MachO doesn't support that
250                // combination. They don't check whether they're targeting
251                // MachO, they check whether the OS is set to Darwin.
252                //
253                // Since both linux and darwin use SysV ABI, this should work.
254                //  but not in the case of Aarch64, there the ABI is slightly different
255                #[allow(clippy::match_single_binding)]
256                match target.triple().architecture {
257                    Architecture::Aarch64(_) => OperatingSystem::Darwin(deployment),
258                    _ => OperatingSystem::Linux,
259                }
260            }
261            other => other,
262        }
263    }
264
265    pub(crate) fn target_binary_format(&self, target: &Target) -> target_lexicon::BinaryFormat {
266        if self.is_pic {
267            target.triple().binary_format
268        } else {
269            match self.target_operating_system(target) {
270                OperatingSystem::Darwin(_) => target_lexicon::BinaryFormat::Macho,
271                _ => target_lexicon::BinaryFormat::Elf,
272            }
273        }
274    }
275
276    fn target_triple(&self, target: &Target) -> TargetTriple {
277        let architecture = if target.triple().architecture
278            == Architecture::Riscv64(target_lexicon::Riscv64Architecture::Riscv64gc)
279        {
280            target_lexicon::Architecture::Riscv64(target_lexicon::Riscv64Architecture::Riscv64)
281        } else {
282            target.triple().architecture
283        };
284        // Hack: we're using is_pic to determine whether this is a native
285        // build or not.
286
287        let operating_system = self.target_operating_system(target);
288        let binary_format = self.target_binary_format(target);
289
290        let triple = Triple {
291            architecture,
292            vendor: target.triple().vendor.clone(),
293            operating_system,
294            environment: target.triple().environment,
295            binary_format,
296        };
297        TargetTriple::create(&triple.to_string())
298    }
299
300    /// Generates the target machine for the current target
301    pub fn target_machine(&self, target: &Target) -> TargetMachine {
302        self.target_machine_with_opt(target, OptimizationStyle::ForSpeed)
303    }
304
305    pub(crate) fn target_machine_with_opt(
306        &self,
307        target: &Target,
308        opt_style: OptimizationStyle,
309    ) -> TargetMachine {
310        let triple = target.triple();
311        let cpu_features = &target.cpu_features();
312
313        match triple.architecture {
314            Architecture::X86_64 | Architecture::X86_32(_) => {
315                InkwellTarget::initialize_x86(&InitializationConfig {
316                    asm_parser: true,
317                    asm_printer: true,
318                    base: true,
319                    disassembler: true,
320                    info: true,
321                    machine_code: true,
322                })
323            }
324            Architecture::Aarch64(_) => InkwellTarget::initialize_aarch64(&InitializationConfig {
325                asm_parser: true,
326                asm_printer: true,
327                base: true,
328                disassembler: true,
329                info: true,
330                machine_code: true,
331            }),
332            Architecture::Riscv64(_) => InkwellTarget::initialize_riscv(&InitializationConfig {
333                asm_parser: true,
334                asm_printer: true,
335                base: true,
336                disassembler: true,
337                info: true,
338                machine_code: true,
339            }),
340            Architecture::LoongArch64 => {
341                InkwellTarget::initialize_loongarch(&InitializationConfig {
342                    asm_parser: true,
343                    asm_printer: true,
344                    base: true,
345                    disassembler: true,
346                    info: true,
347                    machine_code: true,
348                })
349            }
350            _ => unimplemented!("target {} not yet supported in Wasmer", triple),
351        }
352
353        // The CPU features formatted as LLVM strings
354        // We can safely map to gcc-like features as the CPUFeatures
355        // are compliant with the same string representations as gcc.
356        let llvm_cpu_features = cpu_features
357            .iter()
358            .map(|feature| format!("+{feature}"))
359            .join(",");
360
361        let target_triple = self.target_triple(target);
362        let llvm_target = InkwellTarget::from_triple(&target_triple).unwrap();
363        let mut llvm_target_machine_options = TargetMachineOptions::new()
364            .set_cpu(match triple.architecture {
365                Architecture::Riscv64(_) => "generic-rv64",
366                Architecture::LoongArch64 => "generic-la64",
367                _ => "generic",
368            })
369            .set_features(match triple.architecture {
370                Architecture::Riscv64(_) => "+m,+a,+c,+d,+f",
371                Architecture::LoongArch64 => "+f,+d",
372                _ => &llvm_cpu_features,
373            })
374            .set_level(match opt_style {
375                OptimizationStyle::ForSpeed => self.opt_level,
376                OptimizationStyle::ForSize => LLVMOptLevel::Less,
377                OptimizationStyle::Disabled => LLVMOptLevel::None,
378            })
379            .set_reloc_mode(self.reloc_mode(self.target_binary_format(target)))
380            .set_code_model(match triple.architecture {
381                Architecture::LoongArch64 | Architecture::Riscv64(_) => CodeModel::Medium,
382                _ => self.code_model(self.target_binary_format(target)),
383            });
384        if let Architecture::Riscv64(_) = triple.architecture {
385            llvm_target_machine_options = llvm_target_machine_options.set_abi("lp64d");
386        }
387        let target_machine = llvm_target
388            .create_target_machine_from_options(&target_triple, llvm_target_machine_options)
389            .unwrap();
390        target_machine.set_asm_verbosity(self.verbose_asm);
391        target_machine
392    }
393}
394
395impl CompilerConfig for LLVM {
396    fn experimental_artifact(&mut self, enable: bool) {
397        LLVM::experimental_artifact(self, enable);
398    }
399
400    fn max_table_elements(&mut self, max_table_elements: u32) {
401        self.max_table_elements = max_table_elements;
402    }
403
404    /// Emit code suitable for dlopen.
405    fn enable_pic(&mut self) {
406        // TODO: although we can emit PIC, the object file parser does not yet
407        // support all the relocations.
408        self.is_pic = true;
409    }
410
411    fn enable_perfmap(&mut self) {
412        self.enable_perfmap = true
413    }
414
415    fn enable_debugger(&mut self, debugger: Debugger) {
416        self.debugger = Some(debugger)
417    }
418
419    /// For the LLVM compiler, we can use non-volatile memory operations which lead to a better performance
420    /// (but are not 100% SPEC compliant).
421    fn enable_non_volatile_memops(&mut self) {
422        self.enable_non_volatile_memops = true;
423    }
424
425    /// Enables treating eligible funcref tables as read-only so the backend can
426    /// place them in read-only data.
427    fn enable_readonly_funcref_table(&mut self) {
428        self.enable_readonly_funcref_table = true;
429    }
430
431    fn canonicalize_nans(&mut self, enable: bool) {
432        self.enable_nan_canonicalization = enable;
433    }
434
435    /// For the LLVM compiler, enable m0 optimization that passes pointer to the first memory as a hidden first argument.
436    fn enable_m0_pass_param(&mut self, enable: bool) {
437        self.enable_m0 = enable;
438    }
439
440    /// Transform it into the compiler.
441    fn compiler(self: Box<Self>) -> Box<dyn Compiler> {
442        Box::new(LLVMCompiler::new(*self))
443    }
444
445    /// Pushes a middleware onto the back of the middleware chain.
446    fn push_middleware(&mut self, middleware: Arc<dyn ModuleMiddleware>) {
447        self.middlewares.push(middleware);
448    }
449
450    fn supported_features_for_target(&self, _target: &Target) -> wasmer_types::Features {
451        let mut feats = Features::default();
452        feats.exceptions(true);
453        feats.relaxed_simd(true);
454        feats.wide_arithmetic(true);
455        feats.tail_call(true);
456        feats
457    }
458}
459
460impl Default for LLVM {
461    fn default() -> LLVM {
462        Self::new()
463    }
464}
465
466impl From<LLVM> for Engine {
467    fn from(config: LLVM) -> Self {
468        EngineBuilder::new(config).engine()
469    }
470}