JNI 开发:为什么 C++ 在 Debug 正常,Release 却返回 NaN?
本文记录了一次典型的 JNI 坐标转换案例中出现的诡异问题。
一、问题现象描述
1. 现象
- Debug 构建:JNI 返回的坐标数值正常。
- Release 构建:返回坐标中出现
NaN / Infinity,且仅在 Release 模式出现。
2. 出问题的方法
JNIEXPORT void JNICALL Java_com_eqgis_eqr_core_CoordinateUtilsNative_jni_1ToScenePosition(
JNIEnv *env, jclass clazz,
jdouble ref_x, jdouble ref_y,
jdouble target_location_x, jdouble target_location_y,
jdouble azimuth_rad, jdoubleArray outJNIArray) {
double* offset = ComputeTranslation(ref_x, ref_y, target_location_x, target_location_y);
double deX = *offset;
double deY = *(offset + 1);
double x = deX * cos(azimuth_rad) - deY * sin(azimuth_rad);
double y = deX * sin(azimuth_rad) + deY * cos(azimuth_rad);
double outArray[] = {x, y};
env->SetDoubleArrayRegion(outJNIArray, 0, 2, outArray);
}
二、问题根因定位:一个'看起来没问题'的函数
问题最终锁定在这个函数:
double* ComputeTranslation(double x1, double y1, double x2, double x2) {
double res[2] = {0, 0};
res[0] = flagX * x;
res[1] = flagY * y;
return res;
}
乍一看逻辑完全正确,但这里隐藏了一个致命错误。
三、致命问题:返回了栈内存指针(未定义行为)
1. res 是什么?
double res[2];
res 是函数内部的局部变量。
- 存储在当前函数的栈帧(stack frame)中。
2. 函数返回后发生了什么?
当 ComputeTranslation 返回时:
- 函数栈帧被销毁。
res 对应的内存立刻失效。
- 返回的指针指向已被释放的栈空间或即将被复用的内存区域。
3. C++ 标准如何定义这种行为?
这是典型的 Undefined Behavior(未定义行为)。
含义是:
- 编译器不保证任何结果。
- 程序可能'看起来能跑',也可能随机崩溃,也可能只在 Release 模式出问题。
四、为什么 Debug 正常,而 Release 出 NaN?
1. Debug 模式的特点
- 编译器优化极少。
- 栈内存分配保守。
- 局部变量生命周期'看起来'更长,内存内容不容易被覆盖。
- 返回的指针虽然非法,但数据暂时还在。
2. Release 模式的特点
- 启用
-O2 / -O3 等激进优化。
- 栈空间快速复用,指令重排,寄存器替代变量。
- 编译器甚至可能认为:'你返回这个指针是非法的,那我随便优化'。
- 结果就是
*offset 变成随机值,或直接成为 NaN / Inf。
五、为什么 NaN 特别容易出现?
在 Release 下,offset 可能是未初始化内存或被 SIMD / 浮点寄存器覆盖的任意 bit pattern。而浮点数中特定 bit pattern 对应 NaN,一旦参与计算:
NaN + x = NaN
sin(NaN) = NaN
导致后续都是 NaN。
六、这不是 JNI 的问题
值得特别强调的是:
- JNI 只是一个函数调用边界。
- 问题在 C++ 层就已经发生。
- Java 侧只是'忠实地接收了 NaN'。
- 如果这是纯 C++ 工程,现象完全一致。
七、正确修复方式:返回值语义而不是指针
修复方案:使用结构体返回
struct Vec2 {
double x;
double y;
};
Vec2 ComputeTranslation(double x1, double y1, double x2, double y2) {
Vec2 res{0.0, 0.0};
res.x = flagX * x;
res.y = flagY * y;
return res;
}
调用:
Vec2 offset = ComputeTranslation(...);
double deX = offset.x;
double deY = offset.y;
八、为什么'以前这么写也没事'?
原因通常是:
- 旧编译器优化弱。
- Debug 构建长期被使用。
- 数据规模较小。
- 没踩到'恰好会覆盖栈'的场景。
但:未定义行为从来不是'偶尔才错',而是'早晚会炸'。
九、如何系统性避免这类问题?
1. 永远不要返回局部变量地址
return &localVar;
return localArray;
2. 优先使用值语义
struct / std::array / std::pair
3. Debug ≠ 正确
Debug 只能说明:'在当前编译条件下恰好没炸'。
十、总结
| 问题 | 结论 |
|---|
| Debug 正常 | 不代表代码正确 |
| Release 出 NaN | 典型 UB 表现 |
| 根因 | 返回栈内存指针 |
| JNI 是否有问题 | 没有 |
| 正确解法 | 返回结构体 / 值语义 |
这次问题再次验证了一点:C++ 中,最危险的 Bug 往往不是'复杂算法',而是'看起来理所当然的代码'。如果在 Debug / Release 行为不一致时遇到诡异问题,第一时间检查是否触发了未定义行为。