From 281bf700870e546c416742bca4d9e0c02e1d3f67 Mon Sep 17 00:00:00 2001 From: Parker Timmerman Date: Sun, 5 Jan 2025 22:43:51 -0500 Subject: [PATCH] start, add experimental_cross_language_lto feature --- rust/private/lto.bzl | 47 +++++++++++++++++++--- rust/settings/BUILD.bazel | 3 ++ rust/settings/settings.bzl | 11 ++++++ rust/toolchain.bzl | 4 ++ test/unit/lto/lto_test_suite.bzl | 68 +++++++++++++++++++++++++++++++- 5 files changed, 126 insertions(+), 7 deletions(-) diff --git a/rust/private/lto.bzl b/rust/private/lto.bzl index 18fe307515..8517f4a96d 100644 --- a/rust/private/lto.bzl +++ b/rust/private/lto.bzl @@ -44,7 +44,7 @@ rust_lto_flag = rule( ) def _determine_lto_object_format(ctx, toolchain, crate_info): - """Determines if we should run LTO and what bitcode should get included in a built artifact. + """Determines what bitcode should get included in a built artifact. Args: ctx (ctx): The calling rule's context object. @@ -76,13 +76,39 @@ def _determine_lto_object_format(ctx, toolchain, crate_info): # generating object files entirely. return "only_bitcode" elif crate_info.type in ["dylib", "proc-macro"]: - # If we're a dylib and we're running LTO, then only emit object code - # because 'rustc' doesn't currently support LTO with dylibs. - # proc-macros do not benefit from LTO, and cannot be dynamically linked with LTO. + # If we're a dylib or a proc-macro and we're running LTO, then only emit + # object code because 'rustc' doesn't currently support LTO for these targets. return "only_object" else: return "object_and_bitcode" +def _determine_experimental_xlang_lto(ctx, toolchain, crate_info): + """Determines if we should use Linker-plugin-based LTO, to enable cross language optimizations. + + 'rustc' has a `linker-plugin-lto` codegen option which delays LTO to the actual linking step. + If your C/C++ code is built with an LLVM toolchain (e.g. clang) and was built with LTO enabled, + then the linker can perform optimizations across programming language boundaries. + + See + + Args: + ctx (ctx): The calling rule's context object. + toolchain (rust_toolchain): The current target's `rust_toolchain`. + crate_info (CrateInfo): The CrateInfo provider of the target crate. + + Returns: + bool: Whether or not to specify `-Clinker-plugin-lto` when building this crate. + """ + + feature_enabled = toolchain._experimental_cross_language_lto + rust_lto_enabled = toolchain.lto.mode in ["thin", "fat"] + correct_crate_type = crate_info.type in ["bin"] + + # TODO(parkmycar): We could try to detect if LTO is enabled for C code using + # `ctx.fragments.cpp.copts` but I'm not sure how reliable that is. + + return feature_enabled and rust_lto_enabled and correct_crate_type and not is_exec_configuration(ctx) + def construct_lto_arguments(ctx, toolchain, crate_info): """Returns a list of 'rustc' flags to configure link time optimization. @@ -101,10 +127,16 @@ def construct_lto_arguments(ctx, toolchain, crate_info): return [] format = _determine_lto_object_format(ctx, toolchain, crate_info) + xlang_enabled = _determine_experimental_xlang_lto(ctx, toolchain, crate_info) args = [] - # proc-macros do not benefit from LTO, and cannot be dynamically linked with LTO. - if mode in ["thin", "fat", "off"] and not is_exec_configuration(ctx) and crate_info.type != "proc-macro": + # Only tell `rustc` to use LTO if it's enabled, the crate we're currently building has bitcode + # embeded, and we're not building in the exec configuration. + # + # We skip running LTO when building for the exec configuration because the exec config is used + # for local tools, like build scripts or proc-macros, and LTO isn't really needed in those + # scenarios. Note, this also mimics Cargo's behavior. + if mode in ["thin", "fat", "off"] and crate_info.type != "proc-macro" and not is_exec_configuration(ctx): args.append("lto={}".format(mode)) if format == "object_and_bitcode": @@ -117,4 +149,7 @@ def construct_lto_arguments(ctx, toolchain, crate_info): else: fail("unrecognized LTO object format {}".format(format)) + if xlang_enabled: + args.append("linker-plugin-lto") + return ["-C{}".format(arg) for arg in args] diff --git a/rust/settings/BUILD.bazel b/rust/settings/BUILD.bazel index 1dbef8f9b2..69e9706c8a 100644 --- a/rust/settings/BUILD.bazel +++ b/rust/settings/BUILD.bazel @@ -7,6 +7,7 @@ load( "clippy_toml", "codegen_units", "error_format", + "experimental_cross_language_lto", "experimental_link_std_dylib", "experimental_per_crate_rustc_flag", "experimental_use_cc_common_link", @@ -60,6 +61,8 @@ codegen_units() error_format() +experimental_cross_language_lto() + experimental_link_std_dylib() experimental_per_crate_rustc_flag() diff --git a/rust/settings/settings.bzl b/rust/settings/settings.bzl index 1d323ca360..bda9b4fd15 100644 --- a/rust/settings/settings.bzl +++ b/rust/settings/settings.bzl @@ -56,6 +56,17 @@ def lto(): build_setting_default = "unspecified", ) +def experimental_cross_language_lto(): + """A build setting which specifies whether or not to specify `linker-plugin-lto` and perform \ + cross language optimizations. + + See: + """ + bool_flag( + name = "experimental_cross_language_lto", + build_setting_default = False, + ) + def rename_first_party_crates(): """A flag controlling whether to rename first-party crates such that their names \ encode the Bazel package and target name, instead of just the target name. diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl index 97d9e07f4c..8bf276d026 100644 --- a/rust/toolchain.bzl +++ b/rust/toolchain.bzl @@ -698,6 +698,7 @@ def _rust_toolchain_impl(ctx): _rename_first_party_crates = rename_first_party_crates, _third_party_dir = third_party_dir, _pipelined_compilation = pipelined_compilation, + _experimental_cross_language_lto = ctx.attr._experimental_cross_language_lto[BuildSettingInfo].value, _experimental_link_std_dylib = _experimental_link_std_dylib(ctx), _experimental_use_cc_common_link = _experimental_use_cc_common_link(ctx), _experimental_use_global_allocator = experimental_use_global_allocator, @@ -885,6 +886,9 @@ rust_toolchain = rule( "_codegen_units": attr.label( default = Label("//rust/settings:codegen_units"), ), + "_experimental_cross_language_lto": attr.label( + default = Label("//rust/settings:experimental_cross_language_lto"), + ), "_experimental_use_coverage_metadata_files": attr.label( default = Label("//rust/settings:experimental_use_coverage_metadata_files"), ), diff --git a/test/unit/lto/lto_test_suite.bzl b/test/unit/lto/lto_test_suite.bzl index 8ccf47da18..51c7b66b07 100644 --- a/test/unit/lto/lto_test_suite.bzl +++ b/test/unit/lto/lto_test_suite.bzl @@ -2,7 +2,7 @@ load("@bazel_skylib//lib:unittest.bzl", "analysistest") load("@bazel_skylib//rules:write_file.bzl", "write_file") -load("//rust:defs.bzl", "rust_library", "rust_proc_macro") +load("//rust:defs.bzl", "rust_library", "rust_proc_macro", "rust_binary") load( "//test/unit:common.bzl", "assert_action_mnemonic", @@ -86,6 +86,39 @@ _lto_proc_macro_test = analysistest.make( config_settings = {str(Label("//rust/settings:lto")): "thin"}, ) +def _lto_xlang_bin_off(ctx): + return _lto_test_impl(ctx, "off", "no", False) + +_lto_xlang_bin_off_test = analysistest.make( + _lto_xlang_bin_off, + config_settings = { + str(Label("//rust/settings:lto")): "off", + str(Label("//rust/settings:experimental_cross_language_lto")): True, + }, +) + +def _lto_xlang_bin_thin(ctx): + return _lto_test_impl(ctx, "thin", None, True) + +_lto_xlang_bin_thin_test = analysistest.make( + _lto_xlang_bin_thin, + config_settings = { + str(Label("//rust/settings:lto")): "thin", + str(Label("//rust/settings:experimental_cross_language_lto")): True, + }, +) + +def _lto_xlang_lib_off(ctx): + return _lto_test_impl(ctx, "off", "no", False) + +_lto_xlang_lib_off_test = analysistest.make( + _lto_xlang_lib_off, + config_settings = { + str(Label("//rust/settings:lto")): "off", + str(Label("//rust/settings:experimental_cross_language_lto")): False, + }, +) + def lto_test_suite(name): """Entry-point macro called from the BUILD file. @@ -102,6 +135,15 @@ def lto_test_suite(name): ], ) + write_file( + name = "crate_bin", + out = "main.rs", + content = [ + "fn main() {}", + "", + ], + ) + rust_library( name = "lib", srcs = [":lib.rs"], @@ -114,6 +156,12 @@ def lto_test_suite(name): edition = "2021", ) + rust_binary( + name = "binary", + srcs = [":main.rs"], + edition = "2021", + ) + _lto_level_default_test( name = "lto_level_default_test", target_under_test = ":lib", @@ -144,6 +192,21 @@ def lto_test_suite(name): target_under_test = ":proc_macro", ) + _lto_xlang_bin_off_test( + name = "lto_xlang_bin_off_test", + target_under_test = ":binary", + ) + + _lto_xlang_bin_thin_test( + name = "lto_xlang_bin_thin_test", + target_under_test = ":binary", + ) + + _lto_xlang_lib_off_test( + name = "lto_xlang_lib_off_test", + target_under_test = ":lib", + ) + native.test_suite( name = name, tests = [ @@ -153,5 +216,8 @@ def lto_test_suite(name): ":lto_level_thin_test", ":lto_level_fat_test", ":lto_proc_macro_test", + ":lto_xlang_bin_off_test", + ":lto_xlang_bin_thin_test", + ":lto_xlang_lib_off_test", ], )