2 个版本
0.2.3 | 2023 年 8 月 4 日 |
---|---|
0.2.2 | 2023 年 8 月 4 日 |
#674 在 开发工具
1.5MB
7K SLoC
包含 (静态库,1MB) build/lib/libBartleby.a,(Mach-o 可执行文件,285KB) ArchiveWriter.cpp.o,(Mach-o 可执行文件,245KB) Bartleby.cpp.o,(Mach-o 可执行文件,69KB) Bartleby-c.cpp.o,(静态库,110KB) build/lib/libBartleby-c.a,(Mach-o 可执行文件,54KB) Error.cpp.o 以及其他 5 个文件。
Bartleby: 一个符号重命名工具。
此仓库包含巴特比的源代码,这是一个库和工具,可以在多个目标文件和静态库之间重命名符号。
为什么?
如果您开发一个库,您的最终产品将包含
- 对提供给用户使用的符号的定义(“导出”函数等),
- 对您认为是库实现细节且希望隐藏给用户的符号的定义(“私有”函数),
- 对您在依赖项中使用的符号的引用(如果您静态链接,则包括其定义)。
Bartleby 帮助您发布只暴露您希望暴露的符号的静态库,而不会将您的依赖项的符号污染命名空间。它类似于 objcopy --prefix-symbols=__private
,但具有“智能”的(见下文“如何?”部分)自动化选择要前缀的符号(例如,对 puts 的引用不会被重命名为 private_puts
,因为这会导致未解析的符号)。
这有助于解决菱形依赖问题:如果您的库用户想链接到 OpenSSL(它恰好也是您链接的库),您如何确保用户仍能控制他们使用的 OpenSSL 版本,而不会与您的 OpenSSL 使用发生名称冲突?您使用 Bartleby。
如何?
Bartleby 接受一组文件作为输入。这些可以是静态库(.a
)或对象(.o
)。Bartleby 通过遍历所有这些对象(用户直接提供的或包含在存档中的)来确定哪些符号被认为是私有的或不是私有的,并收集每个找到的符号的可见性和定义性。使用这些信息,以下是如何构建符号映射的
- 如果在输入文件中的多个位置找到一个符号,并且至少有一个对象实际定义了它,则该符号被标记为已定义。
- 如果在对象中找到的符号具有全局可见性,则该符号被标记为全局。
最后,对于每个标记为已定义和全局的符号,Bartleby将其视为私有,并为其名称添加前缀,并更改所有对象中对该符号的所有引用。输出是一个包含所有处理对象的单个静态库。
示例
假设我们有以下三个C文件,以及它们的头文件
api_1.c
:
#include <stdio.h>
#include <external_dep.h>
__attribute__((visibility("default"))) void my_api(void) {
puts("my_api called, calling external dep");
char buf[0x41];
external_api(buf, sizeof(buf));
}
void internal_impl(void) {
puts("internal implementation, calling `my_api`");
my_api();
}
api_2.c
#include <stdio.h>
#include "api_1.h"
__attribute__((visibility("default"))) void another_api(void) {
puts("another_api called, calling my_api");
my_api();
}
external_dep.c
#include <stdio.h>
#include <string.h>
__attribute__((visibility("default"))) void external_api(void *src, const size_t n) {
puts("external_api called, calling external private impl");
memset(src, 0, n);
}
void external_private_impl(void) {
puts("external private implementation called");
}
我们编译了API中的前两个对象
$ clang -fvisibility=hidden -c api_1.c api_2.c -isystem.
$ file api_1.o api_2.o
api_1.o: Mach-O 64-bit object arm64
api_2.o: Mach-O 64-bit object arm64
然后,我们创建一个静态库
$ ar rvs libapi.a api_1.o api_2.o
现在,让我们构建我们的简单外部依赖
$ clang -fvisibility=hidden -c external_dep.c
$ ar rvs libexternal.a external_dep.o
libapi.a
是我们想要为其符号添加自定义前缀的库。 libexternal.a
是我们的外部依赖之一,我们希望用户能够链接到 libapi.a
,而不会遇到任何C符号冲突,因为 libexternal
可能被用于另一个位置。我们通过提供 libapi.a
和 libexternal.a
作为输入来运行Bartleby。输出将是 libapi_v1.1.a
$ bartleby --if libapi.a \
--if libexternal.a \
--of libapi_v1.1.a \
--prefix __impl_v1.1_
5 symbol(s) prefixed
libapi_v1.1.a produced.
现在,我们使用 nm
来检查生成的存档
$ nm libapi_v1.1.a
pi_1.o:
U ___impl_v1.1_external_api
0000000000000064 T ___impl_v1.1_internal_impl
0000000000000000 T ___impl_v1.1_my_api
U ___stack_chk_fail
U ___stack_chk_guard
U _puts
0000000000000084 r l_.str
00000000000000a8 r l_.str.1
api_2.o:
0000000000000000 T ___impl_v1.1_another_api
U ___impl_v1.1_my_api
U _puts
0000000000000020 r l_.str
external_dep.o:
0000000000000000 T ___impl_v1.1_external_api
000000000000001c T ___impl_v1.1_external_private_impl
U _puts
0000000000000038 r l_.str
000000000000006b r l_.str.1
现在我们可以看到已定义的符号已经被添加了前缀 __impl_v1.1
。然而,puts
没有添加前缀,因为即使它是一个全局符号,也没有在任何 libapi.a
或 libexternal.a
中找到定义,因此Bartleby没有为其添加前缀。
入门
Bartleby可以用Bazel或CMake编译。
使用CMake
构建Bartleby需要LLVM >=
15.0。请参阅入门指南,以获取有关如何构建LLVM的更多信息,以及LLVM版本发布以获取所有版本。APT软件包也可在LLVM Debian/Ubuntu软件包页面上找到。
$ cmake -B build -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR=/path/to/llvm-15/lib/cmake
$ cmake --build build
$ ./build/bin/bartleby
如果已安装LLVM的更高版本,则必须定义 BARTLEBY_LLVM_VERSION
$ cmake -B build -DCMAKE_BUILD_TYPE=Release -DBARTLEBY_LLVM_VERSION=16.0 -DLLVM_DIR=/path/to/llvm-16/lib/cmake
$ cmake --build build
使用Bazel
强烈推荐使用 bazelisk
。
$ bazelisk build -c opt bartleby/...
$ ./bazel-bin/bartleby/tools/Bartleby/bartleby
将Bartleby导入您的Bazel项目
您还可以使用 http_archive
在现有的Bazel工作区中导入Bartleby。您可以使用它作为工具,也可以通过bartleby starlark规则使用它。有关更多信息,请参阅examples/bazel
。
许可
请参阅LICENSE
。