10 - 库的创建与使用
10 - 库的创建与使用
学习如何创建、安装、版本管理 C/C++ 库,掌握 pkg-config 和 RPATH 机制。
10.1 静态库的完整创建流程
项目结构
mylib/
├── include/
│ └── mylib.h
├── src/
│ ├── math_utils.c
│ └── string_utils.c
├── Makefile
└── example/
└── main.c
头文件
// include/mylib.h
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" {
#endif
// 版本信息
#define MYLIB_VERSION_MAJOR 1
#define MYLIB_VERSION_MINOR 2
#define MYLIB_VERSION_PATCH 0
// 数学工具
int mylib_add(int a, int b);
int mylib_multiply(int a, int b);
double mylib_sum_array(const double *arr, int n);
// 字符串工具
char *mylib_str_reverse(const char *str);
int mylib_str_count_char(const char *str, char c);
#ifdef __cplusplus
}
#endif
#endif /* MYLIB_H */
源文件
// src/math_utils.c
#include "mylib.h"
int mylib_add(int a, int b) {
return a + b;
}
int mylib_multiply(int a, int b) {
return a * b;
}
double mylib_sum_array(const double *arr, int n) {
double sum = 0.0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
return sum;
}
// src/string_utils.c
#include "mylib.h"
#include <stdlib.h>
#include <string.h>
char *mylib_str_reverse(const char *str) {
int len = strlen(str);
char *result = malloc(len + 1);
if (!result) return NULL;
for (int i = 0; i < len; i++) {
result[i] = str[len - 1 - i];
}
result[len] = '\0';
return result;
}
int mylib_str_count_char(const char *str, char c) {
int count = 0;
while (*str) {
if (*str == c) count++;
str++;
}
return count;
}
Makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -Iinclude
AR = ar
ARFLAGS = rcs
STATIC_LIB = libmylib.a
SHARED_LIB = libmylib.so
SRCDIR = src
OBJDIR = obj
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SRCS))
# 静态库
static: $(STATIC_LIB)
$(STATIC_LIB): $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
# 动态库
shared: CFLAGS += -fPIC
shared: $(SHARED_LIB)
$(SHARED_LIB): $(OBJS)
$(CC) -shared -o $@ $^
# 示例程序
example: $(STATIC_LIB)
$(CC) $(CFLAGS) -o example/main example/main.c -L. -lmylib
clean:
rm -rf $(OBJDIR) $(STATIC_LIB) $(SHARED_LIB) example/main
.PHONY: static shared example clean
使用静态库
// example/main.c
#include <stdio.h>
#include <stdlib.h>
#include "mylib.h"
int main(void) {
printf("mylib version: %d.%d.%d\n",
MYLIB_VERSION_MAJOR, MYLIB_VERSION_MINOR, MYLIB_VERSION_PATCH);
printf("2 + 3 = %d\n", mylib_add(2, 3));
printf("4 * 5 = %d\n", mylib_multiply(4, 5));
char *rev = mylib_str_reverse("hello");
printf("reverse: %s\n", rev);
free(rev);
return 0;
}
# 构建静态库
make static
# 生成 libmylib.a
# 构建示例
make example
# 生成 example/main
10.2 动态库的完整创建流程
版本号管理
动态库通常使用三段式版本号:lib<name>.so.MAJOR.MINOR.PATCH
SONAME: libmylib.so.1 ← 主版本号,ABI 不兼容时递增
实际文件名: libmylib.so.1.2.0 ← 完整版本号
开发链接名: libmylib.so ← 符号链接,供编译时使用
兼容性规则:
- MAJOR 递增 → ABI 不兼容,需要重新编译依赖程序
- MINOR 递增 → 新增接口,旧程序无需重新编译
- PATCH 递增 → 修复错误,完全兼容
创建带版本号的动态库
# 编译为 PIC
gcc -fPIC -Wall -Wextra -std=c17 -Iinclude -c src/math_utils.c -o obj/math_utils.o
gcc -fPIC -Wall -Wextra -std=c17 -Iinclude -c src/string_utils.c -o obj/string_utils.o
# 创建带 SONAME 的共享库
gcc -shared -Wl,-soname,libmylib.so.1 \
-o libmylib.so.1.2.0 \
obj/math_utils.o obj/string_utils.o
# 创建符号链接
ln -sf libmylib.so.1.2.0 libmylib.so.1
ln -sf libmylib.so.1 libmylib.so
# 验证
readelf -d libmylib.so.1.2.0 | grep SONAME
# 0x000000000000000e (SONAME) Library soname: [libmylib.so.1]
安装动态库
# 标准安装路径
INSTALL_LIB=/usr/local/lib
INSTALL_INCLUDE=/usr/local/include
# 复制库文件
sudo cp libmylib.so.1.2.0 $INSTALL_LIB/
sudo ln -sf libmylib.so.1.2.0 $INSTALL_LIB/libmylib.so.1
sudo ln -sf libmylib.so.1 $INSTALL_LIB/libmylib.so
# 复制头文件
sudo cp include/mylib.h $INSTALL_INCLUDE/
# 更新动态链接器缓存
sudo ldconfig
# 验证
ldconfig -p | grep mylib
完整的安装 Makefile
PREFIX = /usr/local
LIBDIR = $(PREFIX)/lib
INCLUDEDIR = $(PREFIX)/include
SONAME = libmylib.so.1
REALNAME = libmylib.so.1.2.0
LINKNAME = libmylib.so
install-shared: $(REALNAME)
install -d $(DESTDIR)$(LIBDIR)
install -d $(DESTDIR)$(INCLUDEDIR)
install -m 755 $(REALNAME) $(DESTDIR)$(LIBDIR)/
ln -sf $(REALNAME) $(DESTDIR)$(LIBDIR)/$(SONAME)
ln -sf $(SONAME) $(DESTDIR)$(LIBDIR)/$(LINKNAME)
install -m 644 include/mylib.h $(DESTDIR)$(INCLUDEDIR)/
ldconfig
uninstall:
rm -f $(DESTDIR)$(LIBDIR)/$(REALNAME)
rm -f $(DESTDIR)$(LIBDIR)/$(SONAME)
rm -f $(DESTDIR)$(LIBDIR)/$(LINKNAME)
rm -f $(DESTDIR)$(INCLUDEDIR)/mylib.h
ldconfig
10.3 pkg-config
pkg-config 是一个帮助编译和链接库的工具,自动提供正确的编译和链接选项。
创建 .pc 文件
# mylib.pc
prefix=/usr/local
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: mylib
Description: My utility library
Version: 1.2.0
Cflags: -I${includedir}
Libs: -L${libdir} -lmylib
Libs.private: -lm
安装 .pc 文件
# 标准 pkg-config 路径
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
# 安装 .pc 文件
sudo mkdir -p $PKG_CONFIG_PATH
sudo cp mylib.pc $PKG_CONFIG_PATH/
# 验证
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --modversion mylib
# 1.2.0
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --cflags mylib
# -I/usr/local/include
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --libs mylib
# -L/usr/local/lib -lmylib
使用 pkg-config
# 在 Makefile 中使用
CFLAGS = $(shell pkg-config --cflags mylib)
LDFLAGS = $(shell pkg-config --libs mylib)
# 或在命令行中使用
gcc $(pkg-config --cflags --libs mylib) -o hello main.c
# 检查库是否可用
pkg-config --exists mylib && echo "mylib found"
# 在 CMake 中使用
# find_package(PkgConfig REQUIRED)
# pkg_check_modules(MYLIB REQUIRED mylib)
# target_include_directories(app ${MYLIB_INCLUDE_DIRS})
# target_link_libraries(app ${MYLIB_LIBRARIES})
.pc 文件中的变量
| 变量 | 说明 |
|---|---|
Name | 库名称 |
Description | 库描述 |
Version | 版本号 |
Cflags | 编译选项(头文件路径等) |
Libs | 链接选项(库路径和名称) |
Libs.private | 静态链接时需要的额外库 |
Requires | 依赖的其他包 |
Requires.private | 私有依赖 |
Conflicts | 冲突的包 |
# 示例:依赖另一个包的 .pc 文件
Name: mylib-ext
Description: My extended library
Version: 2.0.0
Requires: mylib >= 1.2.0
Cflags: -I${includedir}
Libs: -L${libdir} -lmylib-ext
10.4 RPATH
RPATH(Runtime Path)嵌入在可执行文件或共享库中,告诉动态链接器在运行时去哪里查找依赖的 .so 文件。
RPATH 选项
# 使用绝对路径
gcc main.c -L. -lmylib -Wl,-rpath,/usr/local/lib -o hello
# 使用 $ORIGIN(相对于可执行文件的位置)
gcc main.c -L. -lmylib -Wl,-rpath,'$ORIGIN/lib' -o hello
# 多个 RPATH
gcc main.c -L. -lmylib \
-Wl,-rpath,'$ORIGIN/lib:/opt/mylib/lib' -o hello
# 查看 RPATH
readelf -d hello | grep -i 'rpath\|runpath'
objdump -x hello | grep -i 'rpath\|runpath'
RPATH vs RUNPATH
| 特性 | RPATH | RUNPATH |
|---|---|---|
| 选项 | -rpath | -rpath + --enable-new-dtags |
| 优先级 | 高于 LD_LIBRARY_PATH | 低于 LD_LIBRARY_PATH |
| 推荐 | 特定场景 | 通常推荐 |
# 使用 RUNPATH(推荐,GCC 默认行为)
gcc main.c -Wl,-rpath,'$ORIGIN/lib' -Wl,--enable-new-dtags -o hello
# 使用 RPATH(覆盖 LD_LIBRARY_PATH)
gcc main.c -Wl,-rpath,'$ORIGIN/lib' -Wl,--disable-new-dtags -o hello
$ORIGIN 的作用
$ORIGIN 替换为可执行文件或库所在的目录:
/opt/myapp/
├── bin/
│ └── hello ← RPATH: $ORIGIN/../lib
└── lib/
└── libmylib.so ← hello 运行时自动找到此文件
$ORIGIN 使得应用可以安装在任意位置而不需要绝对路径
10.5 符号版本控制
版本脚本
# mylib.version
MYLIB_1.0 {
global:
mylib_add;
mylib_multiply;
mylib_str_reverse;
local:
*; # 其他符号不导出
};
MYLIB_1.1 {
global:
mylib_sum_array; # 新增接口
} MYLIB_1.0; # 继承 1.0 的所有导出
# 使用版本脚本创建库
gcc -fPIC -shared -Wl,--version-script=mylib.version \
-Wl,-soname,libmylib.so.1 \
-o libmylib.so.1.1.0 obj/*.o
# 查看版本节点
nm -D libmylib.so.1.1.0 | head
# mylib_add@@MYLIB_1.0
# mylib_multiply@@MYLIB_1.0
# mylib_sum_array@@MYLIB_1.1
10.6 库的 ABI 兼容性
ABI 兼容性检查工具
# 安装 abi-compliance-checker
sudo apt install abi-compliance-checker
# 使用
abi-compliance-checker -lib mylib -old old.xml -new new.xml
# 或使用 abi-dumper
abi-dumper libmylib.so.1.0.0 -o old.dump
abi-dumper libmylib.so.1.1.0 -o new.dump
abi-compliance-checker -l mylib -old old.dump -new new.dump
维护 ABI 兼容性的规则
| 操作 | 兼容性影响 |
|---|---|
| 新增函数 | 兼容(MINOR 递增) |
| 修改函数参数 | 不兼容(MAJOR 递增) |
| 删除函数 | 不兼容(MAJOR 递增) |
| 修改结构体大小 | 不兼容(MAJOR 递增) |
| 修改枚举值 | 可能不兼容 |
| 添加结构体成员(尾部) | 兼容(如果是堆分配) |
| 修改全局变量类型 | 不兼容 |
10.7 C++ 库注意事项
// include/mylib.hpp
#ifndef MYLIB_HPP
#define MYLIB_HPP
#include <string>
#include <vector>
namespace mylib {
class Calculator {
public:
Calculator();
~Calculator();
void add_value(double value);
double get_sum() const;
const std::vector<double> &get_values() const;
private:
struct Impl; // Pimpl 模式隐藏实现
std::unique_ptr<Impl> pimpl;
};
// 导出 C 接口(避免 C++ ABI 问题)
extern "C" {
Calculator *calculator_new();
void calculator_delete(Calc *c);
void calculator_add_value(Calc *c, double value);
double calculator_get_sum(const Calc *c);
}
} // namespace mylib
#endif
C++ 库的 ABI 问题
# GCC 5.1 引入了新的 libstdc++ ABI
# 旧 ABI: std::string 使用 COW(Copy-on-Write)
# 新 ABI: std::string 使用 SSO(Small String Optimization)
# 编译时选择 ABI
g++ -D_GLIBCXX_USE_CXX11_ABI=0 -std=c++17 -o hello main.cpp # 旧 ABI
g++ -D_GLIBCXX_USE_CXX11_ABI=1 -std=c++17 -o hello main.cpp # 新 ABI(默认)
# 检查 ABI 版本
strings libmylib.so | grep GLIBCXX | tail -1
要点回顾
| 要点 | 核心内容 |
|---|---|
| 静态库 | ar rcs 创建,.o 的归档,编译时嵌入 |
| 动态库 | -fPIC -shared 创建,运行时加载,版本管理重要 |
| SONAME | libname.so.MAJOR,ABI 兼容性标记 |
| pkg-config | .pc 文件提供编译和链接选项 |
| RPATH | $ORIGIN 实现可移植的运行时库搜索 |
| 版本脚本 | 控制符号导出,实现多版本兼容 |
注意事项
始终使用
-fPIC创建动态库: 在某些架构上不使用 PIC 编译的代码无法创建共享库。
SONAME 很重要: 没有 SONAME 的共享库在库更新时会导致程序加载错误版本。
RPATH 中
$ORIGIN必须用单引号: shell 会展开$ORIGIN,必须用单引号'保护。
C++ 库的 ABI 稳定性: 建议用 C 接口暴露 C++ 库的公共接口,避免 ABI 兼容性问题。
扩展阅读
- Autotools Mythbuster — GNU 构建系统指南
- Shared Libraries: Understanding Versioning — 共享库版本控制
- pkg-config 文档 — pkg-config 官方文档
- CMake 官方教程 — CMake 库管理
下一步
→ 11 - 交叉编译:深入学习交叉编译的原理、工具链配置和 sysroot 管理。