编译问题汇总与解决方案(二)
在上一篇文章中,我们介绍了由于平台差异和编译器严格性导致的一类编译问题及其解决方法。本篇将继续汇总我们在移植过程中遇到的另一类问题------主要集中在 代码生成、类型转换、Python 集成和底层汇编 等方面。这些问题往往与原构建系统的特殊机制有关,需要我们手动调整以适应 CMake/ESP-IDF 环境。
1. apps 模块中的代码生成问题
NumWorks 的原 Makefile 构建系统在编译 apps 模块时会动态生成一些代码,主要包括:
- 应用列表的宏定义 :根据
EPSILON_APPS变量生成包含所有应用的声明、快照列表等。 - 图标文件 :
icon.cpp在构建过程中由脚本生成,包含各个应用的图标数据。
在迁移到 CMake 后,这些自动生成机制不再工作,导致编译失败。我们的解决方案是将这些生成的内容 直接硬编码 到源文件中。
1.1 应用列表的硬编码
原 Makefile 中通过复杂的字符串替换生成一系列宏定义,例如:
makefile
ruby
EPSILON_APPS ?= calculation graph code statistics distributions inference solver sequence regression elements finance settings
_apps_classes := $(foreach a,$(EPSILON_APPS),$(call capitalize,$a)::App)
_apps_snapshots_declaration := $(foreach a,$(_apps_classes),$a::Snapshot m_snapshot$(subst :,,$a)Snapshot;)
# ... 更多定义
这些宏最终被注入到 apps_container.cpp 等文件的编译选项中。在 CMake 中,我们无法沿用这种动态生成方式,因此直接将展开后的代码写入源文件。
在 apps_container.h 和 apps_container.cpp 中,我们添加了以下内容:
cpp
php
// 包含所有应用的头文件
#include "apps/calculation/app.h"
#include "apps/graph/app.h"
#include "apps/code/app.h"
#include "apps/statistics/app.h"
#include "apps/distributions/app.h"
#include "apps/inference/app.h"
#include "apps/solver/app.h"
#include "apps/sequence/app.h"
#include "apps/regression/app.h"
#include "apps/elements/app.h"
#include "apps/finance/app.h"
#include "apps/settings/app.h"
// 应用总数
#define APPS_CONTAINER_SNAPSHOT_COUNT 12
// 快照列表
#define APPS_CONTAINER_SNAPSHOT_LIST &m_snapshotCalculationAppSnapshot, \
&m_snapshotGraphAppSnapshot, \
&m_snapshotCodeAppSnapshot, \
&m_snapshotStatisticsAppSnapshot, \
&m_snapshotDistributionsAppSnapshot, \
&m_snapshotInferenceAppSnapshot, \
&m_snapshotSolverAppSnapshot, \
&m_snapshotSequenceAppSnapshot, \
&m_snapshotRegressionAppSnapshot, \
&m_snapshotElementsAppSnapshot, \
&m_snapshotFinanceAppSnapshot, \
&m_snapshotSettingsAppSnapshot
// 快照构造函数初始化列表
#define APPS_CONTAINER_SNAPSHOT_CONSTRUCTORS m_snapshotCalculationAppSnapshot(), \
m_snapshotGraphAppSnapshot(), \
m_snapshotCodeAppSnapshot(), \
m_snapshotStatisticsAppSnapshot(), \
m_snapshotDistributionsAppSnapshot(), \
m_snapshotInferenceAppSnapshot(), \
m_snapshotSolverAppSnapshot(), \
m_snapshotSequenceAppSnapshot(), \
m_snapshotRegressionAppSnapshot(), \
m_snapshotElementsAppSnapshot(), \
m_snapshotFinanceAppSnapshot(), \
m_snapshotSettingsAppSnapshot()
// 快照成员变量声明
#define APPS_CONTAINER_SNAPSHOT_DECLARATIONS \
Calculation::App::Snapshot m_snapshotCalculationAppSnapshot; \
Graph::App::Snapshot m_snapshotGraphAppSnapshot; \
Code::App::Snapshot m_snapshotCodeAppSnapshot; \
Statistics::App::Snapshot m_snapshotStatisticsAppSnapshot; \
Distributions::App::Snapshot m_snapshotDistributionsAppSnapshot; \
Inference::App::Snapshot m_snapshotInferenceAppSnapshot; \
Solver::App::Snapshot m_snapshotSolverAppSnapshot; \
Sequence::App::Snapshot m_snapshotSequenceAppSnapshot; \
Regression::App::Snapshot m_snapshotRegressionAppSnapshot; \
Elements::App::Snapshot m_snapshotElementsAppSnapshot; \
Finance::App::Snapshot m_snapshotFinanceAppSnapshot; \
Settings::App::Snapshot m_snapshotSettingsAppSnapshot;
// 应用实例声明
#define APPS_CONTAINER_APPS_DECLARATION \
Calculation::App m_CalculationApp; \
Graph::App m_GraphApp; \
Code::App m_CodeApp; \
Statistics::App m_StatisticsApp; \
Distributions::App m_DistributionsApp; \
Inference::App m_InferenceApp; \
Solver::App m_SolverApp; \
Sequence::App m_SequenceApp; \
Regression::App m_RegressionApp; \
Elements::App m_ElementsApp; \
Finance::App m_FinanceApp; \
Settings::App m_SettingsApp;
这些宏在 apps_container.h 中被使用,确保了应用容器能够正确管理所有内置应用。
1.2 图标文件的处理
原构建过程中,icon.cpp 是在 build 目录下由脚本生成的。由于 CMake 没有自动执行该脚本,我们采取了最简单的办法:将生成的 icon.cpp 文件手动复制到源码目录,并加入版本控制。这样,它就能像普通源文件一样参与编译。
如果未来需要修改图标,只需重新运行生成脚本并更新该文件即可。
2. 类型转换问题
在多个地方,编译器要求更严格的类型匹配,特别是当期望的参数类型为 int32_t 而实际传入的是枚举或整数时。
2.1 表达式构建器的参数
原代码中存在类似这样的调用:
cpp
css
Expression::Builder(n).createLayout(...)
其中 n 的类型可能是 size_t 或枚举,而 Expression::Builder 的某个重载期望 int32_t。我们添加了显式转换:
cpp
css
Expression::Builder(int32_t(n)).createLayout(...)
2.2 用户表达式中的类似问题
另一处:
cpp
css
{.KA = userGridUnit.expression(), .KB = UserExpression::Builder(k)}
同样将 k 转换为 int32_t:
cpp
css
{.KA = userGridUnit.expression(), .KB = UserExpression::Builder(int32_t(k))}
这种显式转换避免了编译器因隐式类型转换可能产生的歧义或警告。
3. Python 模块编译问题
NumWorks 集成了 MicroPython,其中包含一个名为 urandom 的模块。但在 ESP32-S3 上,MicroPython 的某些版本或配置中,该模块可能被命名为 random。编译时遇到符号未定义错误,检查发现模块名称不匹配。
解决方案是将模块的 __name__ 属性从 "urandom" 改为 "random":
cpp
objectivec
// 原代码
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_urandom) },
// 修改后
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_random) },
这样,MicroPython 在导入 random 时就能正确找到该模块。如果应用代码中使用了 import urandom,也需要相应修改,但 NumWorks 的应用通常使用 random,因此影响不大。
4. 汇编代码报错
在 MicroPython 的底层异常处理代码中,有一段针对 Xtensa 架构的汇编实现(nlr_push)。由于我们的目标平台是 ESP32-S3(同样是 Xtensa 架构),本应可以使用该汇编。但在实际编译中,该段汇编代码导致了链接错误或运行时异常。
经过排查,发现可能是寄存器使用与编译器生成的代码冲突,或栈帧布局不符合预期。由于时间有限,我们暂时将这段汇编代码 注释掉 ,转而使用 C 实现的回退版本(nlr_setjmp)。这通过在 py/mpconfig.h 中定义 MICROPY_NLR_SETJMP = 1 来实现。
c
arduino
unsigned int nlr_push(nlr_buf_t *nlr) {
// 原汇编代码被注释
/*
__asm volatile (
"s32i.n a0, a2, 8 \n"
"s32i.n a1, a2, 12 \n"
...
"j nlr_push_tail \n"
);
*/
// 使用 setjmp 实现
return nlr_push_tail(nlr);
}
这样虽然损失了一点性能,但保证了稳定性。后续如有必要,可以重新调试汇编版本。
5. 总结
本篇涵盖了我们在移植过程中遇到的另一类典型问题:
- 构建系统差异导致的手动代码生成与硬编码。
- 类型严格性引发的显式转换需求。
- MicroPython 模块命名不一致的调整。
- 底层汇编的暂时禁用。
这些问题的解决进一步推进了移植工作的完成。目前,NumWorks 的核心功能已经能够在 ESP32-S3 上编译并运行,后续我们将进行更全面的功能测试和性能优化。
希望本系列文章能为同样进行嵌入式移植的开发者提供有益的参考。如有任何疑问或建议,欢迎交流讨论。