rl_locomotion 编译过程四

生成.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 路径)在编译阶段分化。

相关推荐
鸿乃江边鸟6 小时前
Starrocks BE 在Mac编译以及遇到的问题解决
starrocks·mac·编译
特立独行的猫a6 小时前
Fast DDS & Fast DDS Spy Windows x64 编译安装完全指南
windows·编译·安装·fastdds·fastddsspy
dozenyaoyida2 天前
RISC-V嵌入式开发:彻底解决“undefined reference to isatty“错误全攻略
经验分享·c·cmake·嵌入式开发·isatty·没有定义问题
shanql3 天前
CMake笔记:Linux下常规使用
cmake
zh_xuan4 天前
Android JNI 动态注册:获取系统内存页大小
android·cmake·jni·ndk·动态注册·内存页大小
雪靡5 天前
Visual Studio 2026 优雅的给Cmake设置大代理
c++·ide·cmake·visual studio
郝学胜-神的一滴6 天前
CMake 011:跨平台动态库编译
开发语言·c++·嵌入式硬件·qt·程序人生·cmake·liunx
周淳APP7 天前
【前端工程化原理通识:从源头到运行时的理论阐述】
前端·编译·打包·前端工程化
长沙红胖子Qt12 天前
关于 Qt5编译工程出现无限循环qmake编译 的解决方法
编译·循环qmake·一直qmake