生成.os的具体方法和过程
生成 .so
生成 .so 文件的核心语句是 第 54 行:
cmake
pybind11_add_module(${subdir} raisimGymTorch/env/raisim_gym.cpp raisimGymTorch/env/Yaml.cpp)
这一行在 FOREACH(subdir ${SUBDIRS}) 循环中(第 53 行),会被执行两次:
| 循环轮次 | ${subdir} 值 |
生成的 .so 文件 |
|---|---|---|
| 第 1 轮 | rsg_a1_task |
rsg_a1_task.cpython-38-x86_64-linux-gnu.so |
| 第 2 轮 | dagger_a1 |
dagger_a1.cpython-38-x86_64-linux-gnu.so |
pybind11_add_module 做了什么
这是 pybind11 提供的 CMake 函数,内部封装了:
less
普通 cmake: add_library(${subdir} SHARED raisim_gym.cpp Yaml.cpp)
pybind11: pybind11_add_module(${subdir} raisim_gym.cpp Yaml.cpp)
↑
额外自动做了:
① 找到 Python headers 路径并加入 include
② 找到 Python libs 并链接
③ 设置 .so 的文件名后缀为 Python 兼容格式
配套的 3 行(55-58)让编译差异化
仅仅 pybind11_add_module 本身会让两个 .so 完全相同。以下三行让它们成为不同的模块:
cmake
# 第 56 行: 各自 include 自己的 Environment.hpp
target_include_directories(${subdir} PUBLIC
${EIGEN3_INCLUDE_DIRS}
${RAISIMGYM_ENV_DIR}/${subdir} # ← 关键!不同子目录
)
# 第 57 行: 编译选项
target_compile_options(${subdir} PRIVATE -mtune=native -fPIC -O3 -g -mno-avx2)
# 第 58 行: 通过宏注入模块名(决定 PYBIND11_MODULE 的名字)
target_compile_definitions(${subdir} PRIVATE
"-DRAISIMGYM_TORCH_ENV_NAME=${subdir}" # ← 关键!不同模块名
)
一句话 :第 54 行 pybind11_add_module 是生成 .so 的直接语句;第 56 行决定了 Environment.hpp 来自哪个子目录;第 58 行决定了 pybind11 模块的导出名称。三者配合,用同一份模板代码编译出两个功能不同的 Python 扩展。
生成 .so 的完整原理
c
pybind11_add_module(${subdir} raisimGymTorch/env/raisim_gym.cpp raisimGymTorch/env/Yaml.cpp)
1. pybind11_add_module 是什么
这是 pybind11 提供的 CMake 函数(定义在 pybind11/tools/pybind11Tools.cmake),本质上是对 CMake 原生 add_library 的封装:
c
# pybind11_add_module 内部等效于:
add_library(${subdir} MODULE raisim_gym.cpp Yaml.cpp) # MODULE = 动态库(.so),不导出lib
set_target_properties(${subdir} PROPERTIES
PREFIX "" # 去掉 lib 前缀 → 不是 librsg_a1_task.so
SUFFIX ".cpython-38-x86_64-linux-gnu.so" # Python 兼容的文件名后缀
)
target_include_directories(${subdir} PRIVATE
${PYTHON_INCLUDE_DIRS} # → /path/to/conda/include/python3.8
${pybind11_INCLUDE_DIRS} # → thirdParty/pybind11/include
)
target_link_libraries(${subdir} PRIVATE
${PYTHON_LIBRARIES} # → libpython3.8.so
)
关键区别 :add_library(... MODULE ...) 生成的是运行时动态加载的模块(.so),不生成对应的导入库(.a),这与 add_library(... SHARED ...) 不同。
2. 这个命令在项目中的两次执行
less
FOREACH(subdir ${SUBDIRS}) # subdir = rsg_a1_task, 然后 dagger_a1
pybind11_add_module(
rsg_a1_task ← ① 目标名 / 输出文件名
raisimGymTorch/env/raisim_gym.cpp ← ② 绑定代码(模板)
raisimGymTorch/env/Yaml.cpp ← ③ YAML 解析
)
...
ENDFOREACH()
两次调用生成的是两个完全独立的编译目标,Makefile 中表现为:
less
# Makefile 中(简化表示)
# 目标 1: rsg_a1_task
rsg_a1_task.cpp.o: raisim_gym.cpp
g++ -c raisim_gym.cpp -DRAISIMGYM_TORCH_ENV_NAME=rsg_a1_task -Ienvs/rsg_a1_task/ ...
rsg_a1_task.yaml.o: Yaml.cpp
g++ -c Yaml.cpp ...
rsg_a1_task.so: rsg_a1_task.cpp.o rsg_a1_task.yaml.o
g++ -shared rsg_a1_task.cpp.o rsg_a1_task.yaml.o -lraisim -lOpenMP -lpython3.8 -o rsg_a1_task.cpython-38.so
# 目标 2: dagger_a1 (完全独立的编译规则)
dagger_a1.cpp.o: raisim_gym.cpp
g++ -c raisim_gym.cpp -DRAISIMGYM_TORCH_ENV_NAME=dagger_a1 -Ienvs/dagger_a1/ ...
dagger_a1.yaml.o: Yaml.cpp
g++ -c Yaml.cpp ...
dagger_a1.so: dagger_a1.cpp.o dagger_a1.yaml.o
g++ -shared dagger_a1.cpp.o dagger_a1.yaml.o -lraisim -lOpenMP -lpython3.8 -o dagger_a1.cpython-38.so
3. 分步详解
以 rsg_a1_task 为例:
Step 1: 预处理
less
g++ -E raisim_gym.cpp
raisim_gym.cpp 中的两行 #include 展开:
cpp
#include "Environment.hpp"
// → 被 -Ienvs/rsg_a1_task/ 解析
// → 展开为 raisimGymTorch/env/envs/rsg_a1_task/Environment.hpp
// → 得到 class ENVIRONMENT { ... 观测146维, reset逻辑, step逻辑 ... }
#include "VectorizedEnvironment.hpp"
// → 展开为 raisimGymTorch/env/VectorizedEnvironment.hpp
// → 得到 template<class ChildEnvironment> class VectorizedEnvironment { ... }
宏替换:
cpp
// 编译参数 -DRAISIMGYM_TORCH_ENV_NAME=rsg_a1_task
PYBIND11_MODULE(RAISIMGYM_TORCH_ENV_NAME, m)
// 展开为:
PYBIND11_MODULE(rsg_a1_task, m)
// pybind11 内部宏再展开为:
PyMODINIT_FUNC PyInit_rsg_a1_task() { ... }
// ↑
// Python import 时查找的入口函数名
Step 2: 编译成目标文件
bash
g++ -c raisim_gym.cpp -o raisim_gym.cpp.o \
-DRAISIMGYM_TORCH_ENV_NAME=rsg_a1_task \ # 模块名宏
-Ienvs/rsg_a1_task/ \ # Environment.hpp 路径
-I/path/to/python3.8/include/ \ # Python.h
-I/path/to/pybind11/include/ \ # pybind11.h
-I/path/to/eigen3/ \ # Eigen
-mtune=native -fPIC -O3 -g -mno-avx2 # 优化标志
g++ -c Yaml.cpp -o Yaml.cpp.o \
-I/path/to/python3.8/include/ \
-fPIC -O3 -g
-fPIC 是关键:生成位置无关代码(Position Independent Code),这是 .so 的必要条件。
Step 3: 链接成 .so
bash
g++ -shared \
raisim_gym.cpp.o \
Yaml.cpp.o \
-lraisim \ # Raisim 物理引擎
-lOpenMP \ # OpenMP 并行
-lpython3.8 \ # Python C API
-o raisimGymTorch/env/bin/rsg_a1_task.cpython-38-x86_64-linux-gnu.so
-shared 生成共享库。链接器做的事:
- 符号解析 :将
.o中的未定义符号(如raisim::World::integrate())与libraisim.so中的定义关联 - 重定位:修正代码中的地址引用为运行时装载地址
- 生成 ELF 头 :包含
.so的 ABI 信息、依赖库列表(NEEDED libraisim.so)
4. .so 文件的内部结构
less
rsg_a1_task.cpython-38-x86_64-linux-gnu.so (ELF 格式)
│
├─ ELF Header
│ ├─ Type: ET_DYN (共享对象)
│ └─ Entry: PyInit_rsg_a1_task ← Python import 时首先调用
│
├─ .text (代码段)
│ ├─ PyInit_rsg_a1_task() ← pybind11 自动生成
│ │ └─ 注册 VectorizedEnvironment<rsg_a1_task::ENVIRONMENT> 类
│ ├─ VectorizedEnvironment::init() ← 创建200个env实例
│ ├─ VectorizedEnvironment::step() ← OpenMP 并行 step
│ ├─ VectorizedEnvironment::observe() ← 观测收集
│ └─ rsg_a1_task::ENVIRONMENT::step() ← 具体环境逻辑
│
├─ .rodata (只读数据)
│ ├─ 类型注册表 (pybind11 类型信息)
│ └─ 方法名表 ("init", "step", "observe", ...)
│
├─ .data / .bss (全局变量)
│
├─ .dynamic (动态链接信息)
│ ├─ NEEDED: libraisim.so
│ ├─ NEEDED: libpython3.8.so
│ └─ NEEDED: libgomp.so (OpenMP)
│
└─ .symtab (符号表)
├─ PyInit_rsg_a1_task ← 导出符号
└─ (其他符号默认隐藏)
5. Python import 时的装载过程
python
from raisimGymTorch.env.bin import rsg_a1_task
CPython 内部执行:
less
1. 搜索文件
raisimGymTorch/env/bin/rsg_a1_task.cpython-38-x86_64-linux-gnu.so
2. dlopen("rsg_a1_task.so", RTLD_NOW)
→ 装载 .so 到进程地址空间
→ 递归装载 NEEDED 依赖: libraisim.so, libgomp.so
→ 符号重定位
3. dlsym(handle, "PyInit_rsg_a1_task")
→ 查找入口函数
4. 调用 PyInit_rsg_a1_task()
→ pybind11 注册模块 rsg_a1_task
→ 注册类 RaisimGymEnv (VectorizedEnvironment<ENVIRONMENT>)
→ 注册所有 .def() 绑定的方法
5. 返回 Python module 对象
→ import 完成,可以使用 rsg_a1_task.RaisimGymEnv(...)
6. 两个 .so 的本质差异
less
$ diff <(nm -D rsg_a1_task.so) <(nm -D dagger_a1.so)
# 相同点:
# - PyInit_* 函数都存在
# - pybind11 注册的类名都是 "RaisimGymEnv"
# - Yaml.cpp 编译出的符号完全一致
# 不同点:
# - 类 VectorizedEnvironment 实例化的模板参数不同
# (rsg_a1_task::ENVIRONMENT vs dagger_a1::ENVIRONMENT)
# - 观测维度不同 (146 vs 2173)
# - observe() 的实现逻辑不同
这两个 .so 是同构异构体 ------相同的框架(VectorizedEnvironment 模板),填充了不同的环境逻辑(各自的 Environment.hpp),通过编译时参数(宏 + include 路径)在编译阶段分化。