use std::cmp::Reverse;
use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use crate::EH_FRAME_SECTION_NAME;
use crate::misc::{CompiledFunctionExt, CompiledKind};
use crate::object::get_object_for_target;
use crate::progress::ProgressContext;
use crate::types::function::Compilation;
use crate::types::module::CompileModuleInfo;
use crate::{
FunctionBodyData, ModuleTranslationState, WASMER_FUNCTION_OFFSETS_SECTION_NAME,
WASMER_TRAP_FUNCTION_OFFSETS_SECTION_NAME, translator::ModuleMiddleware,
};
use crossbeam_channel::unbounded;
use enumset::EnumSet;
use itertools::Itertools;
use libwild::{
Args, FileSystem, FileType, InputFileData, Linker, OutputFileData, OutputOptions, error,
};
use object::write::{Relocation, StandardSegment, Symbol as ObjSymbol, SymbolSection};
use object::{
RelocationEncoding, RelocationFlags, RelocationKind, SectionFlags, SectionKind, SymbolFlags,
SymbolKind, SymbolScope, elf,
};
use std::{boxed::Box, sync::Arc};
use wasmer_types::{
CompilationProgressCallback, Features, FunctionIndex, LocalFunctionIndex,
entity::{EntityRef, PrimaryMap},
error::CompileError,
target::{CpuFeature, Target, UserCompilerOptimizations},
};
use wasmer_types::{FunctionType, SignatureIndex};
#[cfg(feature = "translator")]
use wasmparser::{Validator, WasmFeatures};
#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)]
pub enum Debugger {
#[strum(serialize = "GDB")]
Gdb,
#[strum(serialize = "LLDB")]
Lldb,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)]
#[allow(missing_docs)]
pub enum DeterministicIdComponent {
#[strum(serialize = "llvm")]
Llvm,
#[strum(serialize = "cranelift")]
Cranelift,
#[strum(serialize = "singlepass")]
Singlepass,
#[strum(serialize = "opt0")]
OptNone,
#[strum(serialize = "optl")]
OptLess,
#[strum(serialize = "optd")]
OptDefault,
#[strum(serialize = "opta")]
OptAggressive,
#[strum(serialize = "opts")]
OptSpeed,
#[strum(serialize = "optsz")]
OptSpeedAndSize,
#[strum(serialize = "nan_canon")]
NanCanonicalization,
#[strum(serialize = "non_vol_mem")]
NonVolatileMemops,
#[strum(serialize = "pic")]
Pic,
#[strum(serialize = "ro_ftable")]
ReadonlyFuncrefTable,
#[strum(serialize = "unaligned_mem")]
ExperimentalUnalignedMemoryAccesses,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)]
pub enum ArtifactFormat {
#[strum(serialize = "rkyv")]
Rkyv,
#[strum(serialize = "native")]
Native,
}
pub trait CompilerConfig {
fn experimental_artifact(&mut self, _enable: bool) {}
fn enable_pic(&mut self) {
}
fn enable_verifier(&mut self) {
}
fn enable_perfmap(&mut self) {
}
fn enable_debugger(&mut self, _debugger: Debugger) {
}
fn enable_non_volatile_memops(&mut self) {}
fn enable_experimental_unaligned_memory_accesses(&mut self) {}
fn enable_readonly_funcref_table(&mut self) {}
fn canonicalize_nans(&mut self, _enable: bool) {
}
fn compiler(self: Box<Self>) -> Box<dyn Compiler>;
fn default_features_for_target(&self, target: &Target) -> Features {
self.supported_features_for_target(target)
}
fn supported_features_for_target(&self, _target: &Target) -> Features {
Features::default()
}
fn push_middleware(&mut self, middleware: Arc<dyn ModuleMiddleware>);
}
impl<T> From<T> for Box<dyn CompilerConfig + 'static>
where
T: CompilerConfig + 'static,
{
fn from(other: T) -> Self {
Box::new(other)
}
}
pub trait Compiler: Send + std::fmt::Debug {
fn name(&self) -> &str;
fn deterministic_id(&self) -> String;
fn artifact_format(&self) -> String {
ArtifactFormat::Rkyv.to_string()
}
fn with_opts(
&mut self,
suggested_compiler_opts: &UserCompilerOptimizations,
) -> Result<(), CompileError> {
_ = suggested_compiler_opts;
Ok(())
}
#[cfg(feature = "translator")]
fn validate_module(&self, features: &Features, data: &[u8]) -> Result<(), CompileError> {
let mut wasm_features = WasmFeatures::empty();
wasm_features.set(WasmFeatures::BULK_MEMORY, features.bulk_memory);
wasm_features.set(WasmFeatures::THREADS, features.threads);
wasm_features.set(WasmFeatures::REFERENCE_TYPES, features.reference_types);
wasm_features.set(WasmFeatures::MULTI_VALUE, features.multi_value);
wasm_features.set(WasmFeatures::SIMD, features.simd);
wasm_features.set(WasmFeatures::TAIL_CALL, features.tail_call);
wasm_features.set(WasmFeatures::MULTI_MEMORY, features.multi_memory);
wasm_features.set(WasmFeatures::MEMORY64, features.memory64);
wasm_features.set(WasmFeatures::EXCEPTIONS, features.exceptions);
wasm_features.set(WasmFeatures::EXTENDED_CONST, features.extended_const);
wasm_features.set(WasmFeatures::RELAXED_SIMD, features.relaxed_simd);
wasm_features.set(WasmFeatures::WIDE_ARITHMETIC, features.wide_arithmetic);
wasm_features.set(WasmFeatures::TAIL_CALL, features.tail_call);
wasm_features.set(WasmFeatures::MUTABLE_GLOBAL, true);
wasm_features.set(WasmFeatures::SATURATING_FLOAT_TO_INT, true);
wasm_features.set(WasmFeatures::FLOATS, true);
wasm_features.set(WasmFeatures::SIGN_EXTENSION, true);
wasm_features.set(WasmFeatures::GC_TYPES, true);
let mut validator = Validator::new_with_features(wasm_features);
validator
.validate_all(data)
.map_err(|e| CompileError::Validate(format!("{e}")))?;
Ok(())
}
fn compile_module(
&self,
target: &Target,
module: &CompileModuleInfo,
compile_info_blob: &[u8],
module_translation: &ModuleTranslationState,
function_body_inputs: PrimaryMap<LocalFunctionIndex, FunctionBodyData<'_>>,
progress_callback: Option<&CompilationProgressCallback>,
) -> Result<Compilation, CompileError>;
fn get_middlewares(&self) -> &[Arc<dyn ModuleMiddleware>];
fn enable_readonly_funcref_table(&self) -> bool {
false
}
fn get_cpu_features_used(&self, cpu_features: &EnumSet<CpuFeature>) -> EnumSet<CpuFeature> {
*cpu_features
}
fn get_perfmap_enabled(&self) -> bool {
false
}
fn get_debugger(&self) -> Option<Debugger> {
None
}
}
pub struct FunctionBucket<'a> {
functions: Vec<(LocalFunctionIndex, &'a FunctionBodyData<'a>)>,
pub size: usize,
}
impl<'a> FunctionBucket<'a> {
pub fn new() -> Self {
Self {
functions: Vec::new(),
size: 0,
}
}
}
pub fn build_function_buckets<'a>(
function_body_inputs: &'a PrimaryMap<LocalFunctionIndex, FunctionBodyData<'a>>,
bucket_threshold_size: u64,
) -> Vec<FunctionBucket<'a>> {
let mut function_bodies = function_body_inputs
.iter()
.sorted_by_key(|(id, body)| Reverse((body.data.len(), id.as_u32())))
.collect_vec();
let mut buckets = Vec::new();
while !function_bodies.is_empty() {
let mut next_function_body = Vec::with_capacity(function_bodies.len());
let mut bucket = FunctionBucket::new();
for (fn_index, fn_body) in function_bodies.into_iter() {
if bucket.size + fn_body.data.len() <= bucket_threshold_size as usize
|| bucket.size == 0
{
bucket.size += fn_body.data.len();
bucket.functions.push((fn_index, fn_body));
} else {
next_function_body.push((fn_index, fn_body));
}
}
function_bodies = next_function_body;
buckets.push(bucket);
}
buckets
}
pub trait CompiledFunction {}
pub trait FuncTranslator {}
#[allow(clippy::too_many_arguments)]
pub fn translate_function_buckets<'a, C, T, F, G>(
pool: &rayon::ThreadPool,
func_translator_builder: F,
translate_fn: G,
progress: Option<ProgressContext>,
buckets: &[FunctionBucket<'a>],
) -> Result<Vec<C>, CompileError>
where
T: FuncTranslator,
C: CompiledFunction + Send + Sync,
F: Fn() -> T + Send + Sync + Copy,
G: Fn(&mut T, &LocalFunctionIndex, &FunctionBodyData) -> Result<C, CompileError>
+ Send
+ Sync
+ Copy,
{
let progress = progress.as_ref();
let functions = pool.install(|| {
let (bucket_tx, bucket_rx) = unbounded::<&FunctionBucket<'a>>();
for bucket in buckets {
bucket_tx.send(bucket).map_err(|e| {
CompileError::Resource(format!("cannot allocate crossbeam channel item: {e}"))
})?;
}
drop(bucket_tx);
let (result_tx, result_rx) =
unbounded::<Result<Vec<(LocalFunctionIndex, C)>, CompileError>>();
pool.scope(|s| {
let worker_count = pool.current_num_threads().max(1);
for _ in 0..worker_count {
let bucket_rx = bucket_rx.clone();
let result_tx = result_tx.clone();
s.spawn(move |_| {
let mut func_translator = func_translator_builder();
while let Ok(bucket) = bucket_rx.recv() {
let bucket_result = (|| {
let mut translated_functions = Vec::new();
for (i, input) in bucket.functions.iter() {
let translated = translate_fn(&mut func_translator, i, input)?;
if let Some(progress) = progress {
progress.notify_steps(input.data.len() as u64)?;
}
translated_functions.push((*i, translated));
}
Ok(translated_functions)
})();
if result_tx.send(bucket_result).is_err() {
break;
}
}
});
}
});
drop(result_tx);
let mut functions = Vec::with_capacity(buckets.iter().map(|b| b.functions.len()).sum());
for _ in 0..buckets.len() {
match result_rx.recv().map_err(|e| {
CompileError::Resource(format!("cannot allocate crossbeam channel item: {e}"))
})? {
Ok(bucket_functions) => functions.extend(bucket_functions),
Err(err) => return Err(err),
}
}
Ok(functions)
})?;
Ok(functions
.into_iter()
.sorted_by_key(|x| x.0)
.map(|(_, body)| body)
.collect_vec())
}
pub const WASM_LARGE_FUNCTION_THRESHOLD: u64 = 100_000;
pub const WASM_TRAMPOLINE_ESTIMATED_BODY_SIZE: u64 = 1_000;
pub struct CompiledObjects {
pub object_files: Vec<Vec<u8>>,
pub import_trampoline_object_files: Vec<Vec<u8>>,
pub trampoline_object_files: Vec<Vec<u8>>,
pub dynamic_trampoline_object_files: Vec<Vec<u8>>,
}
fn emit_wasmer_meta_object(
target: &Target,
compile_info_blob: &[u8],
compiled_objects: &CompiledObjects,
) -> Result<Vec<u8>, String> {
let mut obj = get_object_for_target(target.triple())
.map_err(|e| format!("failed to create Wasmer meta object: {e}"))?;
let section_id = obj.add_section(
obj.segment_name(StandardSegment::Data).to_vec(),
crate::WASMER_MODULE_INFO_SECTION_NAME.to_vec(),
SectionKind::Other,
);
obj.append_section_data(section_id, compile_info_blob, 8);
obj.section_mut(section_id).flags = SectionFlags::Elf {
sh_type: elf::SHT_PROGBITS,
sh_flags: elf::SHF_GNU_RETAIN,
};
let section_id = obj.add_section(
obj.segment_name(StandardSegment::Debug).to_vec(),
EH_FRAME_SECTION_NAME.to_vec(),
SectionKind::Debug,
);
obj.append_section_data(section_id, &0u64.to_ne_bytes(), 4);
let section_id = obj.add_section(
obj.segment_name(StandardSegment::Data).to_vec(),
WASMER_FUNCTION_OFFSETS_SECTION_NAME.to_vec(),
SectionKind::Other,
);
obj.section_mut(section_id).flags = SectionFlags::Elf {
sh_type: elf::SHT_PROGBITS,
sh_flags: elf::SHF_GNU_RETAIN,
};
let pointer_size = target
.triple()
.pointer_width()
.map_err(|_| "unknown pointer width".to_string())?
.bytes() as u64;
let pointer_bits = (pointer_size * 8) as u8;
let zero_pointer = vec![0; pointer_size as usize];
let function_offset_names = (0..compiled_objects.object_files.len())
.map(|i| CompiledKind::Local(LocalFunctionIndex::new(i), String::new()).linkage_name())
.chain(
(0..compiled_objects.trampoline_object_files.len()).map(|i| {
CompiledKind::FunctionCallTrampoline(
SignatureIndex::new(i),
FunctionType::new([], []),
)
.linkage_name()
}),
)
.chain(
(0..compiled_objects.dynamic_trampoline_object_files.len()).map(|i| {
CompiledKind::DynamicFunctionTrampoline(
FunctionIndex::new(i),
FunctionType::new([], []),
)
.linkage_name()
}),
);
for function_name in function_offset_names {
let offset = obj.append_section_data(section_id, &zero_pointer, pointer_size);
let symbol_id = obj.add_symbol(ObjSymbol {
name: function_name.to_owned().into(),
value: 0,
size: 0,
kind: SymbolKind::Text,
scope: SymbolScope::Unknown,
weak: false,
section: SymbolSection::Undefined,
flags: SymbolFlags::None,
});
obj.add_relocation(
section_id,
Relocation {
offset,
flags: RelocationFlags::Generic {
kind: RelocationKind::Absolute,
encoding: RelocationEncoding::Generic,
size: pointer_bits,
},
symbol: symbol_id,
addend: 0,
},
)
.map_err(|e| {
format!("failed to add function offset relocation for {function_name}: {e}")
})?;
}
let trap_fn_offsets_section_id = obj.add_section(
obj.segment_name(StandardSegment::Data).to_vec(),
WASMER_TRAP_FUNCTION_OFFSETS_SECTION_NAME.to_vec(),
SectionKind::Other,
);
obj.section_mut(trap_fn_offsets_section_id).flags = SectionFlags::Elf {
sh_type: elf::SHT_PROGBITS,
sh_flags: elf::SHF_GNU_RETAIN,
};
for traps_name in (0..compiled_objects.object_files.len())
.map(|i| CompiledKind::Local(LocalFunctionIndex::new(i), String::new()).traps_name())
{
let offset =
obj.append_section_data(trap_fn_offsets_section_id, &zero_pointer, pointer_size);
let symbol_id = obj.add_symbol(ObjSymbol {
name: traps_name.as_bytes().into(),
value: 0,
size: 0,
kind: SymbolKind::Data,
scope: SymbolScope::Linkage,
weak: true,
section: SymbolSection::Undefined,
flags: SymbolFlags::None,
});
obj.add_relocation(
trap_fn_offsets_section_id,
Relocation {
offset,
flags: RelocationFlags::Generic {
kind: RelocationKind::Absolute,
encoding: RelocationEncoding::Generic,
size: pointer_bits,
},
symbol: symbol_id,
addend: 0,
},
)
.map_err(|e| {
format!("failed to add function trap offset relocation for {traps_name}: {e}")
})?;
}
obj.write()
.map_err(|e| format!("failed to serialize Wasmer meta object: {e}"))
}
#[derive(Clone, Default)]
struct InMemoryFileSystem {
files: Arc<Mutex<HashMap<PathBuf, Arc<Vec<u8>>>>>,
}
#[derive(Debug)]
struct InMemoryInput(Arc<Vec<u8>>);
impl InputFileData for InMemoryInput {
fn bytes(&self) -> &[u8] {
&self.0
}
}
struct InMemoryOutput {
path: PathBuf,
bytes: Vec<u8>,
files: Arc<Mutex<HashMap<PathBuf, Arc<Vec<u8>>>>>,
}
impl OutputFileData for InMemoryOutput {
fn bytes(&self) -> &[u8] {
&self.bytes
}
fn bytes_mut(&mut self) -> &mut [u8] {
&mut self.bytes
}
fn finish(mut self) -> error::Result {
self.files
.lock()
.map_err(|e| format!("cannot lock in-memory FS: {e}"))?
.insert(self.path, Arc::new(std::mem::take(&mut self.bytes)));
Ok(())
}
}
impl FileSystem for InMemoryFileSystem {
type Input = InMemoryInput;
type Output = InMemoryOutput;
fn open_input(&self, path: &Path, _: bool) -> error::Result<(Self::Input, Option<Arc<File>>)> {
let bytes = self
.files
.lock()
.map_err(|e| format!("cannot lock in-memory FS: {e}"))?
.get(path)
.map(Arc::clone)
.ok_or_else(|| error!("No such in-memory file: {}", path.display()))?;
Ok((InMemoryInput(bytes), None))
}
fn file_type(&self, path: &Path) -> error::Result<FileType> {
self.files
.lock()
.map_err(|e| format!("cannot lock in-memory FS: {e}"))?
.contains_key(path)
.then_some(FileType::File)
.ok_or_else(|| error!("no such in-memory file"))
}
fn canonicalize(&self, path: &Path) -> error::Result<PathBuf> {
Ok(path.to_path_buf())
}
fn remove_file(&self, path: &Path) -> error::Result<()> {
self.files
.lock()
.map_err(|e| format!("cannot lock in-memory FS: {e}"))?
.remove(path)
.map(|_| ())
.ok_or_else(|| error!("no such in-memory file"))
}
fn rename_file(&self, path: &Path, new_path: &Path) -> error::Result<()> {
let mut files = self
.files
.lock()
.map_err(|e| format!("cannot lock in-memory FS: {e}"))?;
let bytes = files
.remove(path)
.ok_or_else(|| error!("no such in-memory file"))?;
files.insert(new_path.to_path_buf(), bytes);
Ok(())
}
fn create_output(
&self,
path: Arc<Path>,
options: OutputOptions,
) -> error::Result<Self::Output> {
let size = usize::try_from(options.size).map_err(|_| error!("output is too large"))?;
Ok(InMemoryOutput {
path: path.to_path_buf(),
bytes: vec![0; size],
files: Arc::clone(&self.files),
})
}
fn write_auxiliary(&self, path: &Path, bytes: &[u8]) -> error::Result {
self.files
.lock()
.map_err(|e| format!("cannot lock in-memory FS: {e}"))?
.insert(path.to_path_buf(), Arc::new(bytes.to_vec()));
Ok(())
}
}
const WASMER_IMAGE_FILENAME: &str = "wasmer-image.so";
const WASMER_META_FILENAME: &str = "__wasmer_meta.o";
pub fn emit_metadata_and_link(
pool: &rayon::ThreadPool,
target: &Target,
compile_info_blob: &[u8],
compiled_objects: CompiledObjects,
mut debug_dir: Option<PathBuf>,
module_hash: Option<String>,
) -> Result<Vec<u8>, CompileError> {
pool.install(|| {
let meta_object = emit_wasmer_meta_object(target, compile_info_blob, &compiled_objects)
.map_err(CompileError::Codegen)?;
let CompiledObjects {
object_files,
import_trampoline_object_files,
trampoline_object_files,
dynamic_trampoline_object_files,
} = compiled_objects;
let fs = InMemoryFileSystem::default();
let mut link_args = vec![
"ld".to_string(),
"-Bsymbolic".to_string(),
"-shared".to_string(),
"-z".to_string(),
"now".to_string(),
"-z".to_string(),
"relro".to_string(),
"-o".to_string(),
WASMER_IMAGE_FILENAME.to_string(),
];
{
let mut files = fs
.files
.lock()
.map_err(|e| CompileError::Codegen(format!("cannot lock in-memory FS: {e}")))?;
for (index, object) in object_files
.into_iter()
.chain(import_trampoline_object_files)
.chain(trampoline_object_files)
.chain(dynamic_trampoline_object_files)
.enumerate()
{
let path = PathBuf::from(format!("object-{index}.o"));
files.insert(path.clone(), Arc::new(object));
link_args.push(path.display().to_string());
}
files.insert(PathBuf::from(WASMER_META_FILENAME), Arc::new(meta_object));
}
link_args.push(WASMER_META_FILENAME.to_string());
let mut wild_args = Args::new(|| link_args.iter().map(String::as_str)).map_err(|e| {
CompileError::Codegen(format!("failed to initialize Wild linker: {e:?}"))
})?;
wild_args
.parse(|| link_args.iter().map(String::as_str))
.map_err(|e| {
CompileError::Codegen(format!("failed to parse Wild linker args: {e:?}"))
})?;
Linker::with_file_system(fs.clone())
.run(&wild_args)
.map_err(|e| CompileError::Codegen(format!("Wild linker failed: {e:?}")))?;
let image = fs
.files
.lock()
.map_err(|e| CompileError::Codegen(format!("cannot lock in-memory FS: {e}")))?
.remove(Path::new(WASMER_IMAGE_FILENAME))
.ok_or_else(|| CompileError::Codegen("Wild linker did not produce an output".into()))?;
let image = Arc::try_unwrap(image).map_err(|_| {
CompileError::Codegen("Wild linker retained a reference to the output buffer".into())
})?;
if let Some(debug_dir) = debug_dir.as_mut() {
if let Some(ref hash) = module_hash {
debug_dir.push(hash);
}
std::fs::create_dir_all(&debug_dir).ok();
debug_dir.push(WASMER_IMAGE_FILENAME);
let _ = std::fs::write(debug_dir, &image);
}
Ok(image)
})
}