【问题反馈】JNI 开发:为什么 C++ 在 Debug 正常,Release 却返回 NaN?

【问题反馈】JNI 开发:为什么 C++ 在 Debug 正常,Release 却返回 NaN?
摘要:
在 Android NDK / JNI 开发中,经常会遇到这样一种“诡异”问题:Debug 模式下运行完全正常,而 Release 模式却出现 NaN、Infinity 甚至随机结果。
本文通过一次真实的 JNI 坐标转换案例,深入分析了该问题的根本原因——C++ 返回局部栈内存指针所导致的未定义行为(Undefined Behavior)。

请添加图片描述

【问题反馈】JNI 开发:为什么 C++ 在 Debug 正常,Release 却返回 NaN?

本文为以下问题的解决记录。由于问题较为典型,故梳理备忘。
https://github.com/eqgis/Sceneform-EQR/discussions/16

一、问题现象描述

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 y2){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++ 工程:

  • 现象 完全一致

七、正确修复方式:返回值语义而不是指针

修复方案:使用结构体返回

structVec2{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;

八、为什么“以前这么写也没事”?

原因通常是:

  1. 旧编译器优化弱
  2. Debug 构建长期被使用
  3. 数据规模较小
  4. 没踩到“恰好会覆盖栈”的场景

但:

未定义行为从来不是“偶尔才错”,而是“早晚会炸”。

九、如何系统性避免这类问题?

1. 永远不要返回局部变量地址

return&localVar;return localArray;

2. 优先使用值语义

struct/ std::array / std::pair 

3. Debug ≠ 正确

Debug 只能说明:

“在当前编译条件下恰好没炸”

十、总结

问题结论
Debug 正常不代表代码正确
Release 出 NaN典型 UB 表现
根因返回栈内存指针
JNI 是否有问题没有
正确解法返回结构体 / 值语义

这次问题再次验证了一点:

C++ 中,最危险的 Bug 往往不是“复杂算法”,
而是“看起来理所当然的代码”。

如果在 Debug / Release 行为不一致时遇到诡异问题,
第一时间检查:是否触发了未定义行为。


Read more

把废弃的腾讯云服务器改为 Openclaw 仅需一句话!!!(附带免费白嫖AI模型)

把废弃的腾讯云服务器改为 Openclaw 仅需一句话!!!(附带免费白嫖AI模型)

大家好,我是热爱探索AI前沿技术的LucianaiB。 前面我尝试了,感兴趣的可以才是部署一下试试 1.在 Windows 上部署 Openclaw:https://mp.weixin.qq.com/s/iF3ED1e649kkmdR26Y1xiw 2.把 Openclaw 接入到 Moltbook:https://mp.weixin.qq.com/s/QUrB50iwRGGdkl1LO-Tl8Q 相信很多技术爱好者都有这样的经历:趁着双十一或者大促,脑子一热买了一台腾讯云或者阿里云的服务器。买的时候雄心勃勃,想着要搭建博客、跑脚本、做图床。结果呢?大概率是跑了几个自动化签到脚本后,它就静静地躺在控制台里“吃灰”,每个月白白扣费。 但是在自己的电脑运行 Openclaw 无法做到24小时的在运行,于是我就想到了我有一个好久不用的腾讯云服务器,之前购买主要是跑一些自动化签到脚本,并没有实际做什么具体工作。于是我就想到把废弃的腾讯云服务器改为 Openclaw 的24小时的服务器。 于是,

By Ne0inhk
Java 面试必问:JVM 运行时数据区域详解(附内存结构图)

Java 面试必问:JVM 运行时数据区域详解(附内存结构图)

文章目录 * 1. 程序计数器(线程私有) * 2. Java 虚拟机栈(线程私有) * 3. 本地方法栈(线程私有) * 4. 堆(线程共享) * 5. 方法区(线程共享) 在 Java 面试中,JVM 内存结构几乎是必问知识点。很多人可以背出程序计数器、虚拟机栈、本地方法栈、堆和方法区这五个区域,但当面试官继续追问它们的作用、线程共享关系以及对象为什么在堆上时,往往就难以解释清楚。本文主要面对面试八股文速成选手,将从 JVM 的整体内存布局出发,带你系统梳理五大运行时数据区域的作用与常见面试考点。 JVM 运⾏时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称 JMM)完全不同,属于完全不同的两个概念,它由以下 5 ⼤部分组成:

By Ne0inhk
如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.preferIPv4Stack=true

如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.preferIPv4Stack=true

如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.preferIPv4Stack=true 引言 在开发过程中,我们常常使用集成开发环境(IDE)如 IntelliJ IDEA 或 JetBrains DataGrip 来与数据库进行交互。然而,有时可能会遇到无法连接数据库的情况,尤其是当使用新版的 IDEA 或 DataGrip 时。这种问题通常是由于网络配置或者 IDE 与数据库之间的兼容性问题引起的。 一种常见的解决办法是添加 JVM 参数 -Djava.net.preferIPv4Stack=true,以优先使用 IPv4 协议栈。这种方式能够有效解决因 IPv6 配置问题导致的数据库连接失败问题。本文将详细介绍如何通过修改 IDEA 或 DataGrip 的启动参数来解决这个问题。 文章目录 * 如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.

By Ne0inhk
人工智能|大模型—— 开发 ——Agent Skills设计详解

人工智能|大模型—— 开发 ——Agent Skills设计详解

一、什么是Agent Skills         在与 AI Agent 协作开发时,我们常常希望它能遵循一些特定的、可复用的操作流程,比如按照固定格式创建 Git Release、执行项目代码检查、或是生成符合团队规范的文档。OpenCode Agent Skill 提供了一种机制,允许我们将这些可复用的指令和行为封装起来,供 Agent 在需要时发现并调用。         一个 Skill 本质上是一份包含了特定指令的 Markdown 文件,它定义了一项任务的名称、描述以及具体的执行步骤。通过这种方式,我们可以将复杂的、重复性的工作流程标准化,让 Agent 能够像调用工具一样,精确、一致地执行这些预定义的任务。这不仅提升了协作效率,也确保了输出结果的规范性。         总而言之,Skills的核心价值在于:把重复的指令打包,按需加载。 二、opencode配置skill 创建一个 Skill 的过程非常直接,核心是在指定的目录中放置一个名为 SKILL.

By Ne0inhk