diff --git a/src/compiler.rs b/src/compiler.rs index 16feadc..cb94a89 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -96,6 +96,7 @@ pub(crate) struct BuildSettings { opt_level: OptLevel, debug_level: DebugLevel, use_wasm_opt: bool, + openmp: bool, } /// A single user-supplied token destined for the link stage. Flags (`-Wl`, @@ -266,6 +267,7 @@ pub(crate) fn link_only(args: Vec, mut user_settings: UserSettings) -> R opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: user_settings.run_wasm_opt.unwrap_or(true), + openmp: false, }; let state = State { @@ -553,20 +555,30 @@ fn build_link_args( push("-lm"); push("-lpthread"); push("-lutil"); - - if state.cxx || state.user_settings.include_cpp_symbols { - push("-lc++"); - push("-lc++abi"); - if state.user_settings.wasm_exceptions.is_enabled() { - push("-lunwind"); - } - } } if matches!(module_kind, ModuleKind::DynamicMain) { push("--no-whole-archive"); } + // -fopenmp is a compiler-driver flag and cannot be forwarded to wasm-ld. + // The caller supplies libomp's search path just like any other -L path. + if state.build_settings.openmp { + push("-lomp"); + } + + // The WASIX libomp archive references the C++ runtime. Shared modules do + // not otherwise receive it, and executables should receive only one copy. + if state.build_settings.openmp + || (module_kind.is_executable() && (state.cxx || state.user_settings.include_cpp_symbols)) + { + push("-lc++"); + push("-lc++abi"); + if state.user_settings.wasm_exceptions.is_enabled() { + push("-lunwind"); + } + } + // Link as much as needed out of libclang_rt.builtins regardless of module kind. push("-lclang_rt.builtins-wasm32"); @@ -880,6 +892,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }, args: PreparedArgs { compiler_args: Vec::new(), @@ -907,6 +920,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }, args: PreparedArgs { compiler_args: Vec::new(), @@ -934,6 +948,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }, args: PreparedArgs { compiler_args: Vec::new(), @@ -962,6 +977,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }, args: PreparedArgs { compiler_args: Vec::new(), @@ -989,6 +1005,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }, args: PreparedArgs { compiler_args: Vec::new(), @@ -1017,6 +1034,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }, args: PreparedArgs { compiler_args: Vec::new(), @@ -1045,6 +1063,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }, args: PreparedArgs { compiler_args: Vec::new(), @@ -1076,6 +1095,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }, args: PreparedArgs { compiler_args: Vec::new(), @@ -1137,6 +1157,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }, args: PreparedArgs { compiler_args: Vec::new(), @@ -1235,6 +1256,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: false, + openmp: false, }, args: PreparedArgs { compiler_args: Vec::new(), @@ -1260,6 +1282,23 @@ mod tests { .collect() } + fn compiler_args_state(args: &[&str], cxx: bool) -> State { + let mut user_settings = UserSettings::default(); + let (args, build_settings) = prepare_compiler_args( + args.iter().map(|arg| (*arg).to_owned()), + &mut user_settings, + cxx, + ) + .unwrap(); + State { + user_settings, + build_settings, + args, + cxx, + temp_dir: PathBuf::from("/tmp"), + } + } + #[test] fn test_dynamic_main_user_lib_emitted_after_whole_archive_block() { // Regression: for DynamicMain, wasixcc force-includes the sysroot libs via @@ -1296,6 +1335,54 @@ mod tests { ); } + #[test] + fn test_openmp_links_runtime_for_shared_module() { + let state = compiler_args_state(&["-fopenmp", "-shared", "ext.o", "-o", "out.wasm"], false); + assert!(state.build_settings.openmp); + let args = rendered_link_args(&state); + + for library in ["-lomp", "-lc++", "-lc++abi", "-lunwind"] { + assert_eq!( + args.iter().filter(|arg| arg.as_str() == library).count(), + 1, + "OpenMP should link {library} once, got {args:?}" + ); + } + } + + #[test] + fn test_openmp_cxx_executable_does_not_duplicate_cpp_runtime() { + let mut state = link_args_state( + ModuleKind::StaticMain, + vec![LinkToken::Input(PathBuf::from("main.o"))], + ); + state.build_settings.openmp = true; + state.cxx = true; + let args = rendered_link_args(&state); + + for library in ["-lomp", "-lc++", "-lc++abi", "-lunwind"] { + assert_eq!( + args.iter().filter(|arg| arg.as_str() == library).count(), + 1, + "OpenMP C++ executables should link {library} once, got {args:?}" + ); + } + } + + #[test] + fn test_no_openmp_leaves_runtime_out() { + let state = link_args_state( + ModuleKind::SharedLibrary, + vec![LinkToken::Input(PathBuf::from("ext.o"))], + ); + let args = rendered_link_args(&state); + + assert!( + !args.iter().any(|arg| arg == "-lomp"), + "libomp must not be linked without OpenMP, got {args:?}" + ); + } + #[test] fn test_user_whole_archive_bracket_contiguous_in_link_args() { // A user --whole-archive --no-whole-archive must reach wasm-ld diff --git a/src/compiler/flags.rs b/src/compiler/flags.rs index 9ab0cb4..bd2221e 100644 --- a/src/compiler/flags.rs +++ b/src/compiler/flags.rs @@ -185,6 +185,10 @@ fn update_build_settings_from_compiler_flag( user_settings.wasm_exceptions = WasmExceptionStyle::Exnref } Flag::Simple("-fno-exceptions") => user_settings.wasm_exceptions = WasmExceptionStyle::Off, + // The link stage invokes wasm-ld directly, so remember the compiler + // driver's OpenMP runtime request and translate it in build_link_args. + Flag::Simple("-fopenmp" | "-fopenmp=libomp") => build_settings.openmp = true, + Flag::Simple("-fno-openmp") => build_settings.openmp = false, Flag::Simple("-fPIC") => user_settings.pic = true, Flag::Simple("-fno-PIC") => user_settings.pic = false, Flag::Simple("--wasm-opt") => build_settings.use_wasm_opt = true, @@ -455,6 +459,7 @@ pub(super) fn prepare_compiler_args( opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }; let args = DEFAULT_WASM_CLANG_FLAGS @@ -557,6 +562,7 @@ mod tests { opt_level: OptLevel::O0, debug_level: DebugLevel::G0, use_wasm_opt: true, + openmp: false, }; let mut us = UserSettings::default(); update_build_settings_from_compiler_flag(Flag::Simple("-O3"), &mut bs, &mut us); @@ -572,6 +578,11 @@ mod tests { assert_eq!(us.wasm_exceptions, WasmExceptionStyle::Exnref); update_build_settings_from_compiler_flag(Flag::Simple("-fno-exceptions"), &mut bs, &mut us); assert_eq!(us.wasm_exceptions, WasmExceptionStyle::Off); + assert!(!bs.openmp); + update_build_settings_from_compiler_flag(Flag::Simple("-fopenmp"), &mut bs, &mut us); + assert!(bs.openmp); + update_build_settings_from_compiler_flag(Flag::Simple("-fno-openmp"), &mut bs, &mut us); + assert!(!bs.openmp); us = UserSettings::default(); update_build_settings_from_compiler_flag( Flag::Simple("-fwasm-exceptions"), @@ -643,6 +654,42 @@ mod tests { assert!(pa.linker_inputs().is_empty()); } + #[test] + fn test_openmp_flags_flow_through_compiler_arg_processing() { + for enable_flag in ["-fopenmp", "-fopenmp=libomp"] { + let mut us = UserSettings::default(); + let args = vec![enable_flag.to_string(), "input.o".to_string()]; + let (prepared, settings) = prepare_compiler_args(args, &mut us, false).unwrap(); + + assert!( + settings.openmp, + "{enable_flag} should request the OpenMP runtime" + ); + assert!( + prepared.compiler_args.iter().any(|arg| arg == enable_flag), + "{enable_flag} must still reach the compiler frontend" + ); + assert_eq!(prepared.linker_inputs(), vec![PathBuf::from("input.o")]); + } + + let mut us = UserSettings::default(); + let (_, settings) = prepare_compiler_args( + ["-fopenmp", "-fno-openmp", "input.o"].map(str::to_string), + &mut us, + false, + ) + .unwrap(); + assert!(!settings.openmp, "the last OpenMP flag should win"); + + let (_, settings) = prepare_compiler_args( + ["-fno-openmp", "-fopenmp", "input.o"].map(str::to_string), + &mut us, + false, + ) + .unwrap(); + assert!(settings.openmp, "the last OpenMP flag should win"); + } + #[test] fn test_prepare_compiler_args_lib_flag() { let mut us = UserSettings::default();