前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[Bazel]自定义规则实现将多个静态库合并为一个动态库或静态库

[Bazel]自定义规则实现将多个静态库合并为一个动态库或静态库

作者头像
别打名名
发布2020-07-28 15:26:03
5K0
发布2020-07-28 15:26:03
举报
文章被收录于专栏:小白AI.易名小白AI.易名
  • 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):

代码语言:javascript
复制
$ 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 实现规则的理论基础

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

代码语言:javascript
复制
$ gcc -shared -fPIC -Wl,--whole-archive libA.a libB.a libC.a -Wl,--no-whole-archive  -Wl,-soname -o libcombined.so

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

方式一:

代码语言:javascript
复制
$ 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):

代码语言:javascript
复制
# MacOS系统
$ libtool -static -o libcombined.a libA.a libB.a libC.a

在 Unix-like 系统上:

代码语言:javascript
复制
$ 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):

代码语言:javascript
复制
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 工具。需要在规则中传入当前编译器信息:

代码语言:javascript
复制
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 结构体内,这里先不展开如何获取了,这里只做个提示:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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(),
        "output": attr.output()
    },
)

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

代码语言:javascript
复制
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/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/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
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 别打名名 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 前言
  • 2 自定义规则实现
    • 2.1 规则功能
      • 2.2 实现规则的理论基础
        • 2.3 规则代码实现
        • 3 总结
        • 4 参考资料
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档