ELF 共享库重定向

ELF 共享对象要求

在基于 ELF 的系统(如 Linux 和 Fuchsia)上,进入共享对象(即 GN 说话中的 shared_library()loadable_module() 目标)的机器代码必须使用 -fPIC 编译器和链接器选项进行构建。

这与使用 -fPIE 的可执行代码不同。这会生成比 -fPIC 更小、更快的代码,但无法用于共享对象。

Fuchsia build 不支持为 Linux 生成共享库,但对于 Fuchsia,它会定义单独的工具链实例来编译可执行文件和共享库。

这种单独的工具链在内部构建规则中称为“shlib”工具链,甚至是伴生工具链,并且始终通过将 -shared 后缀附加到基本工具链标签来命名。例如,//build/toolchain/fuchsia:x64-shared 是基本 //build/toolchain/fuchsia:x64 工具链的 shlib 工具链。

ELF 共享库重定向

Fuchsia build 实现一项功能,可确保始终在 shlib 工具链中构建 ELF shared_library()loadbable_module() 目标(如上一部分所定义)。

Fuchsia 开发者不需要了解这一点,因为这是完全透明的:照常编写 shared_library() 目标,无需担心 ELF / 非 ELF。

本部分的其余内容将介绍其实现方式,首先需要了解它的需求,并提供一些实际示例:

假设有一个 C++ 静态库(例如 libutil),它可以链接到可执行文件或共享库对象。由于这两种类型的二进制文件都需要使用不同的编译器标记构建代码,因此一种方法是定义两个目标,如下所示:

  # A variant of the library to be linked into executables.
  static_library("libutil-static") {
    sources = [ ... ]
    ...
  }

  # A variant of the library to be linked into shared libraries.
  static_library("libutil-shared") {
    sources = [ ... ]
    ...
    if (is_fuchsia) {
      cflags = [ "-fPIC" ]  # Required for shared library code on Fuchsia.
    }
  }

  executable('program') {
    sources = [ ... ]
    deps = [ ":libutil-static" ]
  }

  shared_library('foo') {
    sources = [ ... ]
    deps = [ ":libutil-shared" ]
  }

此操作可以运行,但也存在一些不便:

  • 该库需要两个目标定义(而不是一个),并且这两个目标需要保持同步。即使在未构建 Fuchsia 二进制文件时。

  • 需要向每个共享变体定义额外添加显式 is_fuchsia 检查和编译器标志,这种做法很细微,很容易出错,而且会使这些定义不太抽象。

    (其中一些可以使用 GN 配置简化,但仍)。

  • 使用该库的任何目标都需要明确选择两个变体之一。

为了稍微提升真实感,我们假设 libutil 库还依赖于另一个 liblog 静态库。后者还需要提供静态变体和共享变体,如下所示:

  static_library("liblog-static") {
    sources = [ ... ]
    ...
  }

  static_library("liblog-shared") {
    sources = [ ... ]
    if (is_fuchsia) {
      cflags = [ "-fPIC" ]
    }
  }

  static_library("libutil-static") {
    ...
    deps = [ ":liblog-static" ]
  }

  static_library("libutil-shared") {
    ...
    if (is_fuchsia) {
      cflags = [ "-fPIC" ]  # Required for shared library code.
    }
    deps = [ ":liblog-shared" ]
  }

  ... same as above

以下依赖关系图说明了这一点:

  program ->
      libutil-static ->
          liblog-static

  foo ->
      libutil-shared ->
          liblog-shared

使所有这些目标定义和依赖项正确保持同步是一项繁琐的工作,并会大大降低构建规则的抽象性和实用性。

使用专用工具链构建 ELF 共享对象代码可避免 libutil 及其依赖项的目标重复。例如:

# The following definition is global and should normally be put
# in the BUILDCONFIG.gn file

if (is_fuchsia) {
  # Name of the toolchain used to build ELF shared library code.
  # This toolchain adds the `-fPIC` option to all build commands
  # by default so there is no need to add it in target definitions.
  shlib_toolchain = "${current_toolchain}-shared"
} else {
  # Shared library code can be built directly in the current toolchain
  # on non-Fuchsia platforms.
  shlib_toolchain = current_toolchain
}

# The following is part of a BUILD.gn file

static_library("liblog") {
  ...
}

static_library("libutil") {
  ...
  deps = [ ":liblog" ]
}

executable('program') {
  sources = [ ... ]
  deps = [ ":libutil" ]
}

shared_library("foo") {
  sources = [ ... ]
  deps = [ ":libutils($shlib_toolchain)" ]
}

现在,它对应于依赖关系图:

  program ->
      libutil ->
          liblog

  foo ->
      libutil($shlib_toolchain) ->
          liblog($shlib_toolchain)

上述方案可以解决大多数原始问题,原因如下:

  • 显式 is_fuchsia 检查和添加 -fPIC 编译器标志已完全独立于静态库定义。

  • libutil 目标无需担心要选择其依赖项的哪个变体。

另一方面,每个 shared_library() 实例仍然需要从 shlib_toolchain 中仔细选择其静态库和源代码集依赖项,以关联到代码的正确变体。

Fuchsia 构建使用了最后一个技巧来解决最后一个问题:在基本工具链中使用重定向组目标,以引用 shblib 工具链中的真实共享库目标,如下所示:

# Set to true if the current toolchain's target platform is based
# on ELF and requires an shlib toolchain. This would normally be
# defined in BUILDCONFIG.gn for Fuchsia toolchains.
_requires_shlib_toolchain = ...

if (_requires_shlib_toolchain && current_toolchain != shlib_toolchain) {
  # A simple group that depends on the same target built in
  # the shblib_toolchain. Note that `public_deps` instead of `deps`
  # is required when crossing toolchain boundaries for proper
  # linking.
  group("bar") {
    public_deps = [ ":bar($shlib_toolchain)" ]
  }
} else {
  # The target that actually builds the shared library in
  # the shlib_toolchain, or the base one for non-Fuchsia platforms.
  # It will pick its dependencies in the same toolchain context.
  shared_library("bar") {
   ...
   deps = [ ":libutil" ]
  }
}

executable("program2") {
  deps = [ ":bar" ]
}

这最终会在 Fuchsia 上创建以下依赖关系图:

  program2 -->
      bar -->                           # redirection group
          bar(shblib_toolchain) -->     # real shared_library()
              libutil(shlib_toolchain)

非 Fuchsia 基础工具链将获得:

  program2 -->
      bar -->                           # real shared_library()
          libutil

为了进一步简化使用,Fuchsia build 在其 BUILDCONFIG.gn 中重新定义 shared_library() 模板,从可以用最自然的方式编写的 BUILD.gn 文件中隐藏此实现细节,如下所示:

### This would appear in BUILDCONFIG.gn to ensure that `shared_library()`
### does ELF shared library redirection automatically when needed.

if (_requires_shlib_toolchain) {
  template("shared_library") {
    if (current_toolchain != shlib_toolchain) {
      group(target_name) {
        public_deps = [ ":${target_name}(${shlib_toolchain})" ]
      }
    } else {
      # Ensure the built-in shared_library() function is called.
      target("shared_library", target_name) {
        forward_variables_from(invoker, "*")
      }
    }
  }
}

### This would appear in regular BUILD.gn files

# Invokes the custom `shared_library()` template instead
# of the built-in one. On Fuchsia systems, this will create a
# redirection group that refers to a real shared_library()
# target in the shlib_toolchain. On a non-Fuchsia system, this
# simply defines a real shared_library() target.

shared_library("bar") {
  ...
  deps = [ ":libutil" ]
}

executable("program2") {
  ...
  deps = [ ":bar" ]
}

如需了解详情,请参阅 BUILDCONFIG.gn 中的 shared_library() 定义