【名名的Bazel笔记】自定义规则实现将多个静态库合并为一个动态库或静态库

2023-05-16

文章目录

    • 1 前言
    • 2 自定义规则实现
      • 2.1 规则功能
      • 2.2 实现规则的理论基础
      • 2.3 规则代码实现
    • 3 总结
    • 4 参考资料

1 前言

为了实现如标题所述的将多个静态库合并为一个动态库,内置的 Bazel 规则是没有这个功能的,Bazel C/C++ 相关的内置规则有:

  • cc_binary :生成可执行文件
  • cc_import :允许用户导入预编译的 C/C++ 库,包括动态库、静态库
  • cc_library :生成动/静态库
  • cc_proto_library :从 .proto 文件生成 C++ 代码
  • fdo_prefetch_hints :表示位于工作区中或位于指定绝对路径的 FDO 预取提示配置文件
  • fdo_profile :表示工作区中或位于指定绝对路径的 FDO 配置文件
  • cc_test :测试 C/C++ 样例
  • cc_toolchain :表示一个 C++ 工具链
  • cc_toolchain_suite :表示 C++ 工具链的集合

而我们知道规则(Rule)定义了 Bazel 对输入执行的一系列操作,以生成一组输出。例如 cc_binary 规则可能:

  • 输入(Inputs):获取一组 .cpp 文件
  • 动作(Action):基于输入运行 g++
  • 输出(Output):返回一个可执行文件

从 Bazel 的角度来看,g++ 和标准 C++ 库也是这个规则的输入。作为规则编写人员,你不仅必须考虑用户提供的规则输入,还必须考虑执行操作(Actions)所需的所有工具和库。比如我们手动的将多个静态库(libA.a、libB.a、libC.a)合并为一个动态库(libcombined.so):

$ gcc -shared -fPIC -Wl,--whole-archive libA.a libB.a libC.a -Wl,--no-whole-archive -Wl,-soname -o libcombined.so

注:-Wl,option 后面接的选项最终会作为链接器 ld 的参数,即上面的命令最终还调用了 ld 命令。而 -Wl,--whole-archive {xxx} -Wl,--no-whole-archive 所包围的库表示将 {xxx} 库列表中所有 .o 中的符号都链接进来,这样会导致链接不必要的代码进来,从而导致生成的库会相对很大。目前还没有找到相关办法是否可以做到只链接进上层模块库所调用到的函数。

在编写规则中我们就需要获取当前的编译器,我们不能直接使用固定的路径,比如 Linux 下 /usr/bin/gcc,因为可能是交叉编译器,路径就不一样了。另外我们还需要传入 gcc 将多个静态库合并成一个动态库的相关参数、待合成的静态库列表、最后要生成的动态库名称和路径。这样就是一个比较完善的自定义规则了。

2 自定义规则实现

2.1 规则功能

  • 将多个静态库合并成一个动态库
  • 将多个静态库合并成一个静态库
  • 可以设置生成库的名称和生成路径
  • 静态库作为规则依赖

2.2 实现规则的理论基础

将多个静态库合并成一个动态库:

$ gcc -shared -fPIC -Wl,--whole-archive libA.a libB.a libC.a -Wl,--no-whole-archive  -Wl,-soname -o libcombined.so

将多个静态库合并成一个静态库:

方式一:

$ cd temp
$ ar x libA.a
$ ar x libB.a
$ ar x libC.a
$ ar rc libcombined.a *.o

用这种方式无法指定库的输出目录。笨方法就是,将每个待合并的静态库都拷贝到目标目录里去,然后一一 ar -x 操作,然后再到目标目录里操作 ar rc。这就涉及到了中间文件的产生,有一个很重要的点就是中间文件的产生只能在当前 Bazel 包中创建。中间文件的创建我们可以使用 File actions.declare_file(filename, *, sibling=None) 声明然后结合 Action 去真实创建。

方式二(需安装libtool):

# MacOS系统
$ libtool -static -o libcombined.a libA.a libB.a libC.a

在 Unix-like 系统上:

$ sudo apt-get install libtool-bin
# 生成的libcombined.a ar -x 解压出来是 libA.a libB.a libC.a ,而不是 *.o 文件。
$ libtool --mode=link gcc -o libcombined.a libA.a libB.a libC.a
# 这样可以指定生成路径,但是 *.o 的生成还是需要 ar -x 来生成
$ libtool --mode=link gcc -o libcombined.a *.o

另外我们需要规则具有参数输入功能,参数输入类型定义可以详见:https://docs.bazel.build/versions/3.4.0/skylark/lib/attr.html ,比如定义一个决定是否合成动态库或静态库的布尔参数(genstatic),以及带依赖项配置(deps):

my_cc_combine = rule(
    implementation = _combine_impl,
    attrs = {
        "genstatic" : attr.bool(default = False),
        "deps": attr.label_list(),
    }
)

Action 描述了如何从一组输入生成一组输出,例如 “在 hello.c 上运行 gcc 并获取 hello.o”。创建操作(Action)时,Bazel 不会立即运行命令。它将其注册在依赖关系图中,因为一个 Action 可以依赖于另一个 Action 的输出(例如,在 C 语言中,必须在编译后调用链接器)。在执行阶段,Bazel 会决定必须以何种顺序运行哪些操作。所有创建 Action 的函数都定义在 ctx.actions 中:

  • ctx.actions.run :运行一个可执行文件
  • ctx.actions.run_shell :运行一个脚本命令
  • ctx.actions.write :将一个字符串写入文件
  • ctx.actions.expand_template :从模板文件中创建一个文件

因此我们可以通过创建一个运行脚本命令的 Action 来运行上面所述的打包命令,即使用 ctx.actions.run_shell 函数。

如前言中讲到的,如果是交叉编译器呢? 那我们还需要在规则中获取到当前编译器的信息,包括 gccldar 工具。需要在规则中传入当前编译器信息:

my_cc_combine = rule(
    implementation = _combine_impl,
    attrs = {
        "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
        "genstatic" : attr.bool(default = False),
        "deps": attr.label_list(),
    }
)

然后在 _combine_impl 中通过 load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain") 中的 find_cpp_toolchain(ctx) 获取当前编译器信息。

还有一个比较重要的问题就是,如果依赖还有依赖呢? 比如 libA.a 依赖了 libD.alibE.a,那我们还需要将 libD.alibE.a 也合并到 libcombined.so 中。这种依赖也分为两种,一种是 libD.a 是外部已经编译好的静态库,而 libE.a 是有 cc_library 规则编译出来的静态库。那如何能够把这两种方式的库都最后合并到 libcombined.so 呢?

depset 是一种专门的数据结构,支持有效的合并操作,并定义了遍历顺序。通常用于从 rules 和 aspects 的传递依赖中积累数据。depset 的成员必须是可散列的(hashable),并且所有元素都是相同类型。具体的其他特性和用法这里就不展开了,我们只需要知道这种数据结构保存了 rules 里目标的依赖关系信息。Depsets 可能包含重复的值,但是使用 to_list() 成员函数可以获取一个没有重复项的元素列表,遍历所以成员。

我们在 _combine_impl 中可以用 ctx.attr.deps 获得当前目标的依赖列表,每个元素的组成为
<target //libA:A, keys:[CcInfo, InstrumentedFilesInfo, OutputGroupInfo]>,即包含一个目标和目标的三个信息体,目标里结构具体可以参考官方文档并获取相关信息,比如用 {Target}.files.to_list() 可以获取 Target 直接生成的一组文件列表,意思就是比如 A 目标,直接生成的就是 libA.a。目标 A 的依赖目标 E 信息在 CcInfo 结构体内,这里先不展开如何获取了,这里只做个提示:

x = dep_target[CcInfo].linking_context.linker_inputs.to_list()
for linker_in in x:
    # <LinkerInput(owner=//libA:A, libraries=[<LibraryToLink(pic_objects=[File:[[<execution_root>]bazel-out/k8-fastbuild/bin]libA/_objs/A/liba.pic.o], pic_static_library=File:[[<execution_root>]bazel-out/k8-fastbuild/bin]libA/libA.a, alwayslink=false)>, ], userLinkFlags=[], nonCodeInputs=[])>
    for linker_in_lib in linker_in.libraries:
        # <generated file libE/libA.a>
        # <generated file libE/libE.a>
        internal_link_lib = linker_in_lib.pic_static_library
        # <source file 3rdparty/libs/libD.a>
        external_link_lib = linker_in_lib.static_library
        

2.3 规则代码实现

my_cc_combine.bzl:

load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")

def _combine_impl(ctx):
    cc_toolchain = find_cpp_toolchain(ctx)    

    target_list = []
    for dep_target in ctx.attr.deps:        
        # CcInfo, InstrumentedFilesInfo, OutputGroupInfo      
        cc_info_linker_inputs = dep_target[CcInfo].linking_context.linker_inputs

        target_dirname_list = []
        for linker_in in cc_info_linker_inputs.to_list():            
            for linker_in_lib in linker_in.libraries:                
                if linker_in_lib.pic_static_library != None:
                    target_list += [linker_in_lib.pic_static_library]                    
                if linker_in_lib.static_library != None:
                    target_list += [linker_in_lib.static_library]
    
    output = ctx.outputs.output
    if ctx.attr.genstatic:
        cp_command  = ""       
        processed_list = []
        processed_path_list = []
        for dep in target_list:
            cp_command += "cp -a " + dep.path + " " + output.dirname + "/ && "
            processed = ctx.actions.declare_file(dep.basename)
            processed_list += [processed]
            processed_path_list += [dep.path]
        cp_command += "echo 'starting to run shell'"
        processed_path_list += [output.path]
  
        ctx.actions.run_shell(
            outputs = processed_list,
            inputs = target_list,
            command = cp_command,
        )

        command = "cd {} && ar -x {} {}".format(
                output.dirname,
                " && ar -x ".join([dep.basename for dep in target_list]),
                " && ar -rc libauto.a *.o"
            )
        print("command = ", command)
        ctx.actions.run_shell(
            outputs = [output],
            inputs = processed_list,
            command = command,
        )
    else:
        command = "export PATH=$PATH:{} && {} -shared -fPIC -Wl,--whole-archive {} -Wl,--no-whole-archive -Wl,-soname -o {}".format(
            cc_toolchain.ld_executable,
            cc_toolchain.compiler_executable,
            " ".join([dep.path for dep in target_list]),
            output.path)
        print("command = ", command)
        ctx.actions.run_shell(
            outputs = [output],
            inputs = target_list,
            command = command,
        )

my_cc_combine = rule(
    implementation = _combine_impl,
    attrs = {
        "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
        "genstatic" : attr.bool(default = False),
        "deps": attr.label_list(allow_files = [".a"]),
        "output": attr.output()
    },
)

BUILD 文件中调用我们创建的规则示例:

load(":my_cc_combine.bzl", "my_cc_combine")

my_cc_combine(
    name = "hello_combined",
    # 这里将所有的静态库合并成一个静态库
    genstatic = True,
    output = "libcombined.a",
    deps = ["//libA:A", "//libB:B", "//libC:C"]
) 

3 总结

至此自定义规则实现完成,中间遇到了一些麻烦,不过最终都解决了,因为 Bazel 的中文社区目前为止并不是很完善,可以说中文资料大都是概念性介绍和简单入门,很多内容都需要参考官方文档或者去 https://groups.google.com/forum/#!forum/bazel-discuss 提问题,有 Bazel bug 的话就只有去 https://github.com/bazelbuild/bazel/issues 提 issue 了。最后在实现自定义规则中将多个静态库合并为一个动态库示例中,这里有几个点我们需要注意下:

  • 在实现我们中间文件的拷贝过程中,如果最后没有实现输出 output Action,那么中间文件也不会产生,这在我调试过程中带给了我一阵疑惑
  • 另外创建的中间文件因为是拷贝过程,实际生成的中间文件,Bazel 已经做了处理,居然是软链接到沙箱(sandbox)源文件,这中间的原理我暂未弄清楚,或许就是沙箱优化
  • 对于交叉编译器,我们必须使用 find_cpp_toolchain(ctx),而不是直接使用 /usr/bin/gcc 等工具链
  • 这里实现自定义规则,我们只使用了 action.run_shell。其他的比如还可以编写测试规则(类名需以_test结尾)、actions.write(适合小文件生成)、actions.expand_template(用模板生成文件)、用 aspect 从依赖中搜集信息等等规则的具体用法

4 参考资料

  • https://docs.bazel.build/versions/3.4.0/skylark/lib/globals.html#rule
  • https://docs.bazel.build/versions/3.4.0/skylark/rules.html
  • https://docs.bazel.build/versions/3.4.0/skylark/lib/actions.html
  • https://docs.bazel.build/versions/3.4.0/skylark/tutorial-creating-a-macro.html
  • https://docs.bazel.build/versions/3.4.0/skylark/depsets.html
  • https://docs.bazel.build/versions/3.4.0/skylark/lib/Target.html
  • https://docs.bazel.build/versions/3.4.0/skylark/lib/attr.html
  • https://docs.bazel.build/versions/master/skylark/lib/Args.html
  • https://docs.bazel.build/versions/3.4.0/rules.html
  • https://docs.bazel.build/versions/3.4.0/skylark/lib/ctx.html
  • https://docs.bazel.build/versions/3.4.0/be/c-cpp.html
  • https://sourceware.org/binutils/docs/ld/Options.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【名名的Bazel笔记】自定义规则实现将多个静态库合并为一个动态库或静态库 的相关文章

  • make, cmake, bazel

    整理收集了一些自动化编译链接工具的使用教程 希望可以在之后能更好的理解和使用它们 1 make Linux 下 Make 命令Linux make Command Explained With Examples使用make命令编译项目文件入
  • 在 Ubuntu 上安装 Bazel

    在 Ubuntu 上安装 Bazel 链接 https github com bazelbuild bazel 本页面介绍了在 Ubuntu 上安装 Bazel 的选项 此外 xff0c 它还提供指向 Bazel 完成脚本和二进制安装程序的
  • bazel在tensorflow中编译报错

    开始根据博文建议用了最新版的bazel 0 28 0 安装成功了 但在tensorflow编译 bazel build tensorflow tools graph transforms transform graph 报错 home ty
  • 如何从单个 java_test() 规则运行 Bazel 中的所有测试?

    我在 Bazel 中添加测试 但我不想为每个测试文件编写测试规则 但是 每个测试规则都需要一个 test class 正在运行的测试类 因此没有简单的方法可以使用单个 java test 规则运行所有测试 有没有一种解决方法可以让我不需要指
  • 如何在bazel规则中获取WORKSPACE目录

    我命令使用 clang 工具 例如clang format clang tidy或生成一个编译数据库 like this 我需要知道 bzl 文件中的 WORKSPACE 目录 我怎样才能获得它 考虑以下示例 我只想打印工作区中所有 src
  • Bazel:具有 JNI 依赖项的 Java 应用程序

    我已经成功构建了我的 JNI 库 jar jni 共享 cc library 包装的 cc library 但我不知道如何构建使用它的 Java 应用程序 我的构建很简单 java binary name OCFTestServer src
  • 张量流构建错误

    我在构建 Tensorflow 1 1 0 时遇到此错误 Starting local Bazel server and connecting to it ERROR home bishal cache bazel bazel bishal
  • 如何在 Bazel 中静态链接系统库?

    如何在大多数静态模式下静态链接系统库 linkstatic 1 我尝试使用 Wl Bstatic lboost thread Wl Bdynamic 或 Wl Bstatic lboost thread Wl Bdynamic 但它们都不起
  • 替代 bazel 中的“`--whole-archive`”

    我想在我的基于 bazel 的 C 项目之一中链接外部静态库 我需要 whole archive 用于链接库 如 gcc 或 g build 的选项 g main cc Wl whole archive lhttp Wl no whole
  • tools/bazel.rc 如何与外部工作区依赖项一起使用?

    如果我将外部 Bazel 项目作为 WORKSPACE 依赖项拉入 并且该项目有一个 tools bazel rc 添加了一些默认构建选项并定义了一些构建 config 选项 那么它到底是如何工作的 构建这些外部构建目标时是否使用这些默认选
  • bazel.rc 中的默认、特定于平台、Bazel 标志

    我想知道特定于平台的默认 Bazel 构建标志是否可能 例如 我们想使用 workspace status command但这必须是 Linux 上的 shell 脚本 并且必须指向 Windows 上的批处理脚本 有没有一种方法可以让我们
  • 如何将优化标志传递给 bazel 构建张量流

    我正在尝试使用 bazel 为 android 构建 TF 我注意到 当我使用 makefile 构建 TF 时 C 代码得到了优化 并且它比 bazel 生成的库快了几乎 2 倍 这可能是什么原因呢 这里是修改后的 tf copts de
  • 构建规则中的 Bazel 环境变量

    我想参考 DirectX SDKBUILD文件 问题是 据我所知 Bazel 仅支持通过 action env DXSDK DIRBazel 的参数 它应该在动作中使用 必须在插件中定义 bzl file 有没有更简单的方法通过将环境变量用
  • Bazel 和 Gradle 有什么区别?

    谷歌刚刚开源的 https github com bazelbuild bazel它的构建工具Bazel https bazel build 这个工具和之前有什么区别Gradle https gradle org 它能做什么 Gradle
  • 如何使用 bazel 中的 make 规则链接库构建

    我已经使用构建了一个 lib so在 bazel 中制定规则 https stackoverflow com questions 58035752 building makefile using bazel 如何将此外部 lib so 链接
  • 巴泽尔的$地点扩张

    我想添加 location 扩展到rules scala for jvm flags我在其中设置依赖项的属性data属性 但失败了 label src java com google devtools build lib worker in
  • 如何使用 bazelisk 安装 bazel

    或者更好的是 如何安装 bazel 我一直使用 cd 进入桌面上的文件夹 然后我使用git clone https github com bazelbuild bazelisk进而 我现在应该做什么 老实说 我没有找到任何真正的说明 我现在
  • Tensorflow构建量化工具-bazel构建错误

    我正在尝试编译量化脚本 如下所述皮特 沃登的博客 https petewarden com 2016 05 03 how to quantize neural networks with tensorflow 但是 在运行以下 bazel
  • Bazel:将编译标志添加到默认 C++ 工具链

    我想向默认的 C 工具链添加一些编译器和链接器标志 以便我构建的所有目标 本地或导入 共享它们 我知道可以定义我自己的工具链 但我不想这样做 因为它非常复杂且容易出错 理想情况下我想要这样的东西 cc toolchain cc defaul
  • Bazel run - 传递主要参数

    我使用 java image 创建了一个图像 但我想将参数传递给我的主函数 即 String args 当我使用 bazel run name of image 命令时 我该如何做到这一点 bazel run your rule arg1

随机推荐