[Bazel]自定義規則實現將多個靜態庫合併爲一個動態庫或靜態庫


  • 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(),
        "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/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


本文分享自微信公衆號 - 別打名名(biedamingming)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章