📑 本页目录(点开跳转)
04 · ABI 与二进制兼容
⏱ 84 分钟 | ⭐ 「编译得过」和「装得上、跑得起来」是三道不同的关,你那个报错断在其中一道
🎯 一句话
头文件写的是「怎么调用」,ABI 定的是「调用时字节怎么摆」—— 后者不写在任何一份代码里,而两边只要理解得不一样,程序就会在三个不同的地方之一坏掉。
最后一种坏法不报错,它只是把你的 batch=128 读成了 159。
⭐ 这一章是设计成能单独读的,不需要前面三章。
🧩 一、先划界:「装不上」其实有两半
⚠️ 「我装不上这个包」是个复合症状,它的两半归两个不同的板块:
| 哪一半 | 长什么样 | 归哪 |
|---|---|---|
| Python 那一半 | 装到哪个环境去了、import 找的是哪个路径、版本怎么解出来的、锁文件 |
《Python 会咬你的地方》 |
| ⭐ 二进制那一半 | undefined symbol、cannot open shared object file、明明装上了却崩 |
这一章 |
这一章的立项依据是站内一句留了半边的话。《AI 全栈》14 在讲「在我机器上能跑」的三种死法时,第二种写着:
⚠️ 系统库不一样:某个 pip 包依赖
libpq/libgomp。你机器上早有(因为你装过别的东西),干净的服务器上没有 →ImportError: libxxx.so.1: cannot open shared object file
那一章给的解药是容器,并且明确写了容器不解决什么,其中第二条是「GPU 驱动 / CUDA 是宿主机的事」。⭐ 也就是说:最高频的那一类装不上,被划到了它自己的范围之外。 这一章接的就是那个位置。
📋 本章要拆的三道关:
| 关 | 什么时候坏 | 典型报错 |
|---|---|---|
| ① 符号对不上 | 链接 / 加载符号时 | undefined symbol: _ZN3c10... |
| ② 库文件找不到 | 加载动态库时 | libxxx.so.1: cannot open shared object file |
| 💀 ③ 布局对不上 | 不坏,只是数据是错的 | 没有报错 |
🧩 二、C++ 的函数名,不是你写的那个
要理解 ① 得先知道一件事:你写的函数名不会原样出现在编译产物里。
C++ 允许重载 —— 同名不同参的函数可以有好几个。但链接器只认名字,不认参数。所以编译器会把参数类型、命名空间、是不是 const 全部编码进名字里,这个过程叫 name mangling(名字修饰)。
// c04_names.cpp
// g++ -O2 -std=c++17 -c c04_names.cpp -o c04_names.o
#include <string>
#include <vector>
int add(int a, int b) { return a + b; } // 重载 ①
double add(double a, double b) { return a + b; } // 重载 ②
int add(int a, int b, int c) { return a + b + c; } // 重载 ③
namespace mylib {
struct Engine {
int run(const std::string& s) { return (int)s.size(); }
int run(const std::vector<int>& v) { return (int)v.size(); }
};
int helper(const Engine&, double) { return 0; }
}
extern "C" { // ⭐ 关掉名字修饰
int add_c(int a, int b) { return a + b; }
}
编译成目标文件,然后把里面的符号列出来(nm 是 binutils 自带的):
$ nm -g --defined-only c04_names.o | grep " T "
0000000000000010 T _Z3adddd
0000000000000000 T _Z3addii
0000000000000020 T _Z3addiii
0000000000000030 T _ZN5mylib6helperERKNS_6EngineEd
0000000000000040 T add_c
同一批符号,加个 -C 让它解回人话:
$ nm -gC --defined-only c04_names.o | grep " T "
0000000000000010 T add(double, double)
0000000000000000 T add(int, int)
0000000000000020 T add(int, int, int)
0000000000000030 T mylib::helper(mylib::Engine const&, double)
0000000000000040 T add_c
⭐ 两条要记住的:
- 三个
add变成了三个不同的符号(_Z3addii/_Z3adddd/_Z3addiii)—— 参数类型就编码在末尾那几个字母里。 extern "C"的那个仍然叫add_c,一个字没变。这就是 01 章那段代码为什么要写extern "C"。
🚦 用 ctypes 亲眼看一次
把这两种函数编进一个动态库,然后从 Python 按名字去找:
# c04_findsym.py
import ctypes, os
lib = ctypes.CDLL(os.path.abspath('c04lib.dll')) # Linux/macOS 换成 ./c04lib.so
for name in ['add_c', 'add_cpp', '_Z7add_cppii']:
try:
f = lib[name]
f.restype = ctypes.c_int
f.argtypes = [ctypes.c_int] * 2
print(f' {name:16} -> 找到了, add(2,3) = {f(2,3)}')
except AttributeError as e:
print(f' {name:16} -> ❌ {type(e).__name__}: {e}')
对应的库这么编:
// c04_lib.cpp
// g++ -O2 -std=c++17 -shared -static c04_lib.cpp -o c04lib.dll
int add_cpp(int a, int b) { return a + b; } // C++ 名字会被修饰
extern "C" int add_c(int a, int b) { return a + b; } // ⭐ 保持原名
实跑输出:
结果对照
⭐ 第二行和第三行是同一个函数。 按你写的名字找 —— 找不到;按修饰后的名字找 —— 找到了。
⚠️ 所以下次看到 undefined symbol: _ZN3c10... 不要被吓到:_Z 开头就是一个被修饰过的 C++ 名字,3c10 是命名空间 c10(PyTorch 的基础库就叫这个名字)。把它丢给 c++filt 就能读:
$ echo '_ZN5mylib6helperERKNS_6EngineEd' | c++filt
mylib::helper(mylib::Engine const&, double)
🧩 三、API 和 ABI 差在哪
现在可以给 ABI 一个准确的说法了。
| API | ABI | |
|---|---|---|
| 规定什么 | 源码怎么写才能调用(函数名、参数、类型) | 编译后的字节怎么摆:符号叫什么名、参数放哪个寄存器、结构体每个字段在第几个字节、异常怎么传 |
| 写在哪 | 头文件、文档 | ⚠️ 哪儿都没写,由编译器 + 编译选项 + 标准库版本共同决定 |
| 不兼容的表现 | 编译报错,一眼能看见 | 链接报错 / 加载报错 / 或者什么都不报 |
| 谁来检查 | 编译器 | ⚠️ 没人(链接器只对名字,不对含义) |
⭐ 一句话:API 兼容 = 能重新编译;ABI 兼容 = 不用重新编译就能换。
⚠️ 能悄悄改变 ABI 的东西比你想的多:编译器版本、C++ 标准版本、标准库的实现和版本、优化选项、是否开异常、结构体里加一个字段、给函数加一个默认参数、给类加一个虚函数……其中大部分都不会让源码编译失败。
🧩 四、第一道关:符号对不上
最经典的一次 ABI 断裂,是 GCC 5 改了 std::string 的内部结构。为了让老程序还能用,libstdc++ 同时保留了两套实现,用宏 _GLIBCXX_USE_CXX11_ABI 切换。
同一份源码,只改这一个宏,看符号名会怎样:
// c04_abi.cpp
#include <string>
int consume(const std::string& s);
int consume(const std::string& s) { return (int)s.size(); }
要点
$ g++ -std=c++17 -c c04_abi.cpp -o abi1.o # 默认 (=1)
$ nm -g --defined-only abi1.o | grep consume
0000000000000000 T _Z7consumeRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
$ g++ -std=c++17 -D_GLIBCXX_USE_CXX11_ABI=0 -c c04_abi.cpp -o abi0.o
$ nm -g --defined-only abi0.o | grep consume
0000000000000000 T _Z7consumeRKSs
💀 同一个函数、同一份源码,两个完全不同的符号名。 解回人话之后更要命,两边看起来几乎一样:
consume(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
consume(std::string const&)
把它们拿去链接,实跑结果:
结果对照
⭐ 这就是 undefined symbol 家族的真面目:不是「函数没实现」,是两边对同一个函数名的编码方式不一致。你去源码里找,函数明明白白写在那儿。
⚠️ 看到 std::__cxx11 出现在报错里,基本可以确定是这个宏对不上。
📋 这件事在 PyTorch 生态里的具体形态:预编译的 libtorch 是用某一套设定编的,你的扩展必须用同一套。本机装的 torch 报告的是:
# c04_torch_abi.py
import torch
print("torch =", torch.__version__)
print("_GLIBCXX_USE_CXX11_ABI =", torch._C._GLIBCXX_USE_CXX11_ABI)
实跑输出:
torch = 2.13.0+cpu
_GLIBCXX_USE_CXX11_ABI = True
⭐ 这个值必须和你编扩展时用的一致,对不上就是链接期一堆 undefined symbol: _ZN3c10...。⚠️ 同理还有编译器版本和 CUDA 版本 —— 那些 -cu121 / -cu124 后缀的轮子就是在标这件事:它们不是「支持 CUDA 12.1」,是「用 CUDA 12.1 编的,二进制只和那一套对得上」。
🛑 读到这里可以停 —— 前半章讲完了(约 27 分钟)。 后半章还有:第二道关:库文件找不到 · 💀 第三道关:什么都不报,数据是错的 · 于是有了 wheel 标签和 manylinux · 📋 一张排查表 回来的时候不用重读,直接从下一节接着看就行。
🧩 五、第二道关:库文件找不到
符号都对,也可能在加载那一步就死了 —— 因为你的库依赖别的库,而那些库不在。
先看一个动态链接的库依赖谁:
$ objdump -p c04dyn.dll | grep "DLL Name"
DLL Name: KERNEL32.dll
DLL Name: msvcrt.dll
DLL Name: libstdc++-6.dll
ldd 更进一步,把每个依赖实际解析到了哪个文件也打出来:
算一算
$ ldd c04dyn.dll
ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ff9ceba0000)
KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ff9cddd0000)
KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ff9cc2e0000)
msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7ff9ccb10000)
libstdc++-6.dll => /mingw64/bin/libstdc++-6.dll (0x7ff93bc50000)
libgcc_s_seh-1.dll => /mingw64/bin/libgcc_s_seh-1.dll (0x7ff9b5cc0000)
libwinpthread-1.dll => /mingw64/bin/libwinpthread-1.dll (0x7ff9a39d0000)
(后面还有几行系统库,略)
⭐ ldd 的正确读法:你要找的不是列出来的那些,是没被列出来的那一行 —— 在 Linux 上缺失的依赖会显示成 => not found。那一行就是你要装的东西。
🚦 ⚠️ 报错指着 A,缺的其实是 A 的依赖
这是这道关最误导人的地方。实跑三种情况:
# c04_load.py
import ctypes, os
p = os.path.abspath('c04dyn.dll')
print('① 直接加载(依赖 libstdc++-6.dll 不在搜索路径里):')
try:
ctypes.CDLL(p); print(' ✅ 成功')
except OSError as e:
print(' ❌', type(e).__name__, ':', str(e)[:110], '...')
print('② 先 os.add_dll_directory 指出依赖在哪,再加载:')
with os.add_dll_directory(r'C:\Users\manaf\mingw64\bin'):
lib = ctypes.CDLL(p)
lib.strlen_cpp.restype = ctypes.c_int
lib.strlen_cpp.argtypes = [ctypes.c_char_p]
print(' ✅ 成功,strlen_cpp(b"hello") =', lib.strlen_cpp(b'hello'))
print('③ 对照:静态链接的那个 dll,什么都不用做:')
ctypes.CDLL(os.path.abspath('c04lib.dll')); print(' ✅ 成功')
实跑输出:
操作步骤
- 直接加载(依赖 libstdc++-6.dll 不在搜索路径里):
- ❌ FileNotFoundError : Could not find module 'C:\ · \c04dyn.dll' (or one of its dependencies) ·
- 先 os.add_dll_directory 指出依赖在哪,再加载:
- ✅ 成功,strlen_cpp(b"hello") = 5
- 对照:静态链接的那个 dll,什么都不用做:
- ✅ 成功
💀 注意 ① 那句报错:它说找不到 c04dyn.dll,可那个文件明明就在那儿。 缺的是它的依赖 libstdc++-6.dll。括号里那句 or one of its dependencies 是唯一的线索,而它太容易被忽略了。
⭐ Linux 上的同一件事长这样:ImportError: libxxx.so.1: cannot open shared object file —— 报错里那个 libxxx.so.1 通常不是你 import 的那个模块,而是它依赖的系统库。这正是《AI 全栈》14 章描述的那个症状。
📋 三种解法,各有代价:
| 解法 | 做什么 | 代价 |
|---|---|---|
| 装上那个系统库 | apt install libgomp1 之类 |
要有权限;换台机器要重来 |
| ⭐ 静态链接(实验 ③) | 把依赖编进自己的库 | 产物变大;某些库不能这么做 |
| ⭐ 打包进 wheel(下一节) | 把依赖的 .so 一起塞进去并改好搜索路径 |
标准做法,auditwheel 干这活 |
⚠️ 一个 Windows 专有的坑:Python 3.8 之后,ctypes / 扩展模块在 Windows 上不再用 PATH 找依赖,要用 os.add_dll_directory()。所以「我把它加进 PATH 了啊」在新版本 Python 上是无效操作 —— 上面实验 ② 用的就是正确写法。
🧩 六、💀 第三道关:什么都不报,数据是错的
前两道关至少会告诉你出事了。这一道不会。
场景很常见:库升级了,加了个字段;而你这边还在用旧头文件编译。
// lib_v2.cpp —— 「库」这一侧(新版:中间插了一个字段)
#include <cstdio>
struct Config {
int version;
long long lr_scale; // ⭐ v2 新加的,插在中间
long long batch;
};
void lib_show(const Config& c) {
std::printf(" [库里读到的] sizeof=%zu version=%d lr_scale=%lld batch=%lld\n",
sizeof(Config), c.version, c.lr_scale, c.batch);
}
// app_v1.cpp —— 「调用方」这一侧(还在用旧头文件)
#include <cstdio>
struct Config {
int version;
long long batch;
};
void lib_show(const Config& c);
int main() {
Config c{7, 128};
std::printf(" [我传进去的] sizeof=%zu version=%d batch=%lld\n",
sizeof(Config), c.version, c.batch);
lib_show(c);
return 0;
}
实跑(连跑三次,结果完全一致):
操作步骤
- 编译:两边都通过
- 链接:成功,零警告
- 运行:
- [我传进去的] sizeof=16 version=7 batch=128
- [库里读到的] sizeof=24 version=7 lr_scale=128 batch=159
- 退出码 = 0
💀💀 batch 传进去是 128,库里读到 159。没有报错,没有警告,退出码 0。
⭐ 机制:调用方认为 Config 是 16 字节(version 在偏移 0,batch 在偏移 8);库认为是 24 字节(batch 在偏移 16)。于是库从偏移 16 开始读 —— 那个位置已经在这个对象之外了,读到的是碰巧躺在那里的东西。而 lr_scale 读到 128,正是调用方本来放 batch 的位置。
⚠️ 两个额外的观察,都是我在做这个实验时撞出来的:
version是对的。 排在新字段前面的字段全都对,后面的全错 —— 所以症状常常是「大部分参数没问题,就某几个不对」,最容易被当成自己代码的 bug。- ⭐ 它有时候会碰巧正确。 我第一版实验里新字段是
int,正好填进了旧结构体因为对齐留下的 4 字节空洞(03 章讲的那种),于是三个字段读出来全对。⚠️ 「测了一下没问题」在这道关上完全不能作为证据 —— 换个字段类型、换个平台、换个编译器,它就变成上面那样。
📋 这就是为什么生态里有那么多看起来很啰嗦的规矩:
| 规矩 | 挡的是什么 |
|---|---|
| 头文件和二进制必须同一个版本,不许混用 | 就是上面这件事 |
库的接口只用不透明指针 + extern "C" 函数,不在接口上暴露结构体 |
结构体布局变了也不影响调用方 |
| PyTorch 扩展要求 torch 版本、编译器、CUDA、ABI 宏全部对齐 | 三道关一起挡 |
| 升级依赖之后重新编译所有扩展,而不是只换一个 | 避免混用 |
🧩 七、于是有了 wheel 标签和 manylinux
上面三道关合起来说明一件事:一个编译好的二进制,不是「哪都能跑」的。 那 pip 装轮子的时候是怎么知道该给你哪一个的?
答案是文件名里写着。本机的规则可以直接问出来:
# c04_tags.py
import sysconfig, sys
print("EXT_SUFFIX =", sysconfig.get_config_var('EXT_SUFFIX'))
print("SOABI =", sysconfig.get_config_var('SOABI'))
print("version =", sys.version.split()[0])
实跑输出:
对照
EXT_SUFFIX = .cp313-win_amd64.pyd
SOABI = cp313-win_amd64
version = 3.13.14
看一眼装好的 numpy,每个编译产物都带着这个后缀:
要点
_pocketfft_umath.cp313-win_amd64.pyd
lapack_lite.cp313-win_amd64.pyd
_umath_linalg.cp313-win_amd64.pyd
bit_generator.cp313-win_amd64.pyd
mtrand.cp313-win_amd64.pyd
共 19 个
pip 认得的完整标签列表也能直接问:
对照
$ python -m pip debug --verbose
Compatible tags: 45
cp313-cp313-win_amd64
cp313-abi3-win_amd64
cp313-none-win_amd64
cp312-abi3-win_amd64
cp311-abi3-win_amd64
·
⭐ 标签有三段:cp313(哪个 Python)· cp313 / abi3 / none(对 ABI 的要求)· win_amd64(哪个平台)。
| 中间那段 | 意思 |
|---|---|
cp313 |
只和 CPython 3.13 的 ABI 对得上,换个小版本就得重编 |
⭐ abi3 |
只用了 Python 的稳定 ABI 子集,一次编译能在多个版本上用(表里 cp34-abi3 到 cp313-abi3 都在,所以本机能装很老的 abi3 轮子) |
none |
不含扩展模块,纯 Python |
⚠️ win_amd64 这一段在 Linux 上要难得多:Windows 和 macOS 各自的系统库比较统一,而 Linux 发行版之间 glibc 版本天差地别 —— 在新系统上编的轮子,拿到老系统上就是「找不到符号」(第①道关(符号对不上)的另一种形态:glibc 的符号是带版本号的)。
⭐ manylinux 就是为这件事发明的标签,思路只有两句:
- 在足够老的 glibc 上编译,这样新系统也能用(向后兼容是单向的)。
- 只许依赖一个很小的白名单里的系统库,白名单之外的依赖必须打包进 wheel 自己带着。
标签的命名换过一次:早期是 manylinux1 / manylinux2010 / manylinux2014 这种按「年份」的写法,后来改成直接写 glibc 版本的 manylinux_x_y 形式(例如 manylinux_2_17、manylinux_2_28),因为按年份根本看不出兼容边界在哪。
⭐ 两套是对应的,manylinux2014 就是 manylinux_2_17。
做这件事的工具叫 auditwheel:它检查你的 wheel 依赖了哪些库、把不在白名单里的复制进去、再把搜索路径改好。
🗓️ 这一小段没法在本机演示 —— 上面的标签输出是 Windows 的,manylinux 是 Linux 侧的规范。⚠️ 规范本身会变(新的 glibc 基线会不断加进来),用之前查一下当前状态。
📋 回到最开头那几个「老是装不上」的包:
| 现象 | 哪一道关 |
|---|---|
pip install 直接开始编译源码(然后失败) |
没有匹配你这套标签的轮子,只能退回源码编译 |
装上了,import 时 undefined symbol: _ZN3c10... |
① 符号对不上(torch 版本 / ABI 宏 / 编译器不一致) |
装上了,import 时 libcudart.so.12: cannot open... |
② 库找不到(CUDA 运行时不在,或版本号不是 12) |
| 装上了,能 import,结果不对或偶发崩溃 | 💀 ③ 布局对不上 |
⭐ 绝大多数情况下最省事的解法是:不要自己编,去找和你这套(Python 版本 + torch 版本 + CUDA 版本 + 平台)完全匹配的预编译轮子。 那些项目通常有一个很长的下载表,⚠️ 表里那些后缀不是装饰,是上面三道关的编号。
🛑 读到这里可以停 —— 已经读了约 57 分钟。 最后一段还有(约 22 分钟):📋 一张排查表 · 检查点与走神救援 回来的时候不用重读,直接从下一节接着看就行。
🧩 八、📋 一张排查表
拿到一个二进制层面的报错,按这个顺序走:
| 症状里有 | 断在 | 先做什么 |
|---|---|---|
undefined symbol: _Z... / undefined reference to |
① 符号 | 把符号丢给 c++filt 读出来;查 torch 版本、编译器版本、_GLIBCXX_USE_CXX11_ABI 是否一致 |
报错里出现 std::__cxx11 |
① 符号 | 基本可以断定是 _GLIBCXX_USE_CXX11_ABI 对不上 |
cannot open shared object file / Could not find module |
② 库文件 | ldd 看哪一行是 not found;⚠️ 缺的通常不是报错里那个名字,是它的依赖 |
| Windows 上「明明加进 PATH 了」 | ② 库文件 | 用 os.add_dll_directory(),3.8 之后 PATH 不管用了 |
pip install 开始编译源码 |
标签 | 没有匹配的轮子;先找预编译版而不是硬编 |
| 没有报错,但结果不对 / 偶发崩溃 | 💀 ③ 布局 | 检查头文件和二进制是不是同一个版本;重新编译所有扩展 |
🔗 这一章连到哪里
| 相关的地方 | 为什么 |
|---|---|
| 《AI 全栈》14 | ⭐ 本章的立项依据:它描述了 libxxx.so.1: cannot open shared object file 这个症状、给了容器这个解药、又明写容器不解决 GPU/CUDA。本章拆的就是它留下的那半边 |
| 《Python 会咬你的地方》 | 「装不上」的另一半:装到哪个环境、import 找哪个路径、版本怎么解、锁文件。⭐ 本章一个字都不碰那一半 |
| 01 章 | 那一章的示例代码里写着 extern "C" 并注了一句「关掉名字修饰,ctypes 才找得到」—— 第二节是这句话的完整解释 |
| 03 章 | 结构体的对齐空洞是那一章讲的;⭐ 第六节那个「新字段正好填进空洞、于是碰巧全对」的坑,就是它在这一章的化身 |
| 05 章 | 写扩展会真的撞上本章三道关;那一章讲注册机制,本章讲为什么编出来的东西装不上 |
| AI 基础设施 00 | 它的「前置知识」清单里把 CUDA 编程列为「❌ 不需要」,而 -cu121 这类轮子的装不上问题落在本章第七节 |
✅ 检查点
- 「我装不上这个包」的两半分别是什么?各归哪个板块?
- 本章拆的三道关分别在什么时候坏?第三道关的报错长什么样?
- 为什么 C++ 的函数名会被改?
c04_names.cpp里三个add编出来的符号分别叫什么? ctypes那个实验里,add_c/add_cpp/_Z7add_cppii三个名字分别是什么结果?说明了什么?- API 兼容和 ABI 兼容各是什么意思?ABI 写在哪份文件里?谁负责检查它?
- 同一份
consume(const std::string&),两种_GLIBCXX_USE_CXX11_ABI下的符号名分别是什么?链接失败时报错长什么样?看到哪个字符串基本能断定是这个问题? - 本机 torch 的
_GLIBCXX_USE_CXX11_ABI是多少?-cu121这种后缀到底在标什么? ldd的正确读法是什么?为什么说「报错指着 A,缺的其实是 A 的依赖」?举出实跑里那句误导人的报错。- Windows 上 Python 3.8 之后为什么「加进 PATH」不管用了?该用什么?
- 结构体布局不一致那个实验:传进去的
batch是多少,库里读到多少?编译、链接、退出码分别是什么情况? - 为什么说「测了一下没问题」在第三道关上不能当证据?(提示:作者第一版实验发生了什么)
- wheel 标签的三段各是什么?
abi3和cp313差在哪? manylinux的两句核心思路是什么?它的命名为什么从manylinux2014改成了manylinux_2_17这种写法?
👀 答案
- Python 那一半(装到哪个环境、
import找哪个路径、版本解析、锁文件)→ 《Python 会咬你的地方》;二进制那一半(undefined symbol、cannot open shared object file、装上却崩)→ 本章。 - ① 符号对不上,在链接 / 加载符号时坏;② 库文件找不到,在加载动态库时坏;③ 布局对不上,⚠️ 不坏,没有任何报错 —— 只是数据是错的。
- 因为 C++ 允许重载(同名不同参),而链接器只认名字不认参数,所以编译器把参数类型、命名空间、是否 const 全编码进名字。三个
add分别是_Z3addii(int,int)、_Z3adddd(double,double)、_Z3addiii(int,int,int)。 add_c找到了(extern "C",名字没变);add_cpp找不到(AttributeError: function 'add_cpp' not found);_Z7add_cppii找到了。后两个是同一个函数 —— 按源码里的名字找不到,按修饰后的名字才找得到。- API 兼容 = 能重新编译(规定源码怎么写);ABI 兼容 = 不用重新编译就能换(规定编译后字节怎么摆:符号名、参数放哪、结构体字段偏移、异常怎么传)。⚠️ ABI 哪份文件里都没写,由编译器 + 编译选项 + 标准库版本共同决定;没人检查它,链接器只对名字不对含义。
- 默认(=1)是
_Z7consumeRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE,=0是_Z7consumeRKSs。链接失败报undefined reference to consume(std::__cxx11::basic_string<...> const&)。看到std::__cxx11基本可以断定是这个宏对不上。 - 本机 torch 2.13.0+cpu,
_GLIBCXX_USE_CXX11_ABI = True。-cu121不是「支持 CUDA 12.1」,是「用 CUDA 12.1 编的,二进制只和那一套对得上」。 - 要找的不是列出来的那些,是没被列出来的那一行 —— Linux 上缺失的依赖显示成
=> not found。实跑那句误导的报错是Could not find module 'C:\...\c04dyn.dll' (or one of its dependencies)—— 它点名的那个文件明明存在,缺的是它的依赖libstdc++-6.dll,唯一线索是括号里那句or one of its dependencies。Linux 上同理:libxxx.so.1通常不是你 import 的模块本身。 - 因为 Python 3.8 之后,Windows 上加载扩展/动态库不再用
PATH解析依赖。要用os.add_dll_directory()(实验 ② 的写法)。 - 传进去
batch=128,库里读到batch=159;lr_scale读到 128(调用方本来放 batch 的位置)。编译两边都通过、链接成功且零警告、退出码 0 —— 三次跑结果完全一致。调用方认为sizeof=16(batch 在偏移 8),库认为sizeof=24(batch 在偏移 16,已经在对象之外)。 - 因为作者第一版实验里新加的字段是
int,正好填进了旧结构体因对齐留下的 4 字节空洞,于是三个字段读出来全对。⚠️ 换个字段类型、换个平台、换个编译器就变成错的 —— 所以「测了一下没问题」完全不能作为证据。另外 排在新字段前面的字段全都对、后面的全错,症状像「就某几个参数不对」。 - 三段:哪个 Python(
cp313)· 对 ABI 的要求(cp313/abi3/none)· 哪个平台(win_amd64)。cp313只和 CPython 3.13 的 ABI 对得上,换小版本要重编;abi3只用稳定 ABI 子集,一次编译能在多个版本上用(本机的兼容标签里cp34-abi3到cp313-abi3都在)。 - 两句:① 在足够老的 glibc 上编译(向后兼容是单向的);② 只许依赖一个很小的白名单里的系统库,白名单外的必须打包进 wheel 自己带着(
auditwheel干这活)。改名是因为按年份的写法根本看不出兼容边界在哪,直接写 glibc 版本才看得出来;两套是对应的,manylinux2014就是manylinux_2_17。
🛑 可以停在这里
⚡ 走神救援
🚪 「编译得过」和「装得上、跑得起来」是三道不同的关:① 符号对不上(链接/加载时报错)· ② 库文件找不到(加载时报错)· 💀 ③ 布局对不上——不报错,数据是错的。
🏷️ 先理解名字:C++ 允许重载而链接器只认名字,所以编译器把参数类型和命名空间全编码进符号里。⭐ 所以
undefined symbol: _ZN3c10...里的_Z就是修饰过的名字,丢给c++filt就能读。⭐ API 兼容 = 能重新编译;ABI 兼容 = 不用重编就能换——而 ABI 哪儿都没写、没人检查。⚠️ 看到
std::__cxx11基本能断定是那个老新 ABI 开关不一致;-cu121不是「支持 CUDA 12.1」而是「用 CUDA 12.1 编的」。第二道关用
ldd查,⭐ 要找的是没列出来的那一行。💀 实测那句「找不到某个 dll」而文件明明就在那儿——缺的是它的依赖,而线索只有报错里那半句「or one of its dependencies」。⚠️ Windows 上 Python 3.8 之后 PATH 不管用,要用专门的 API 加目录。💀💀 第三道关最贵:库的结构体中间插了个字段,调用方还用旧头文件——编译通过、链接零警告、退出码 0,但传进去的参数被读成完全不相干的数。⚠️ 排在新字段前面的字段全对、后面的全错,看起来像「就某几个参数不对」。
⭐ 而且它有时碰巧正确:新字段如果正好填进旧结构体的对齐空洞,读出来全对——所以「测了一下没问题」在这道关上完全不能当证据。
下一节 👉 05-算子怎么注册进PyTorch.md