ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESP32 应用平台:基于 WebAssembly 实现固件与应用分离

ESP32 应用平台:基于 WebAssembly 实现固件与应用分离 ESP32 这颗芯片玩过的人都知道它便宜、能联网、功耗也还行但绝大多数人拿到手之后干的事情都差不多连个传感器上传数据、做个 Web 服务器点个灯、或者刷个 MicroPython 跑点脚本。这些用法本质上都是“烧一次固件干一件事”。固件烧进去是什么功能它就永远是什么功能想换个玩法就得重新编译、重新烧录跟早年功能机时代换手机才能换功能差不多。但手机不是这样的。手机买回来只是一个空壳你装微信它就是聊天工具装相机它就是拍照设备装个游戏它就是游戏机。同一台硬件通过安装不同的应用实现完全不同的用途。那 ESP32 能不能也这么玩能不能让它变成一个“小型应用平台”固件只负责提供基础能力具体功能通过安装应用来实现这个想法听起来有点疯狂毕竟 ESP32 的资源跟手机差了十万八千里。但它确实是可以做的而且做出来之后你会发现这套思路在很多场景下比传统“一固件一功能”的方式要灵活得多。下面我就把这套小型应用平台的完整设计思路、技术选型、踩过的坑和实测效果从头到尾讲一遍。1. 为什么要在 ESP32 上折腾“应用平台”这件事1.1 传统固件开发模式的真实痛点先说说为什么我会想到做这个东西。我手头有几个基于 ESP32 的小项目一个是阳台上的环境监测节点一个是车库门的远程控制器还有一个是鱼缸的补光和喂食控制。这三个东西硬件上其实差不多都是 ESP32 模组加几个传感器和执行器但每个项目的固件都是独立编译的。问题出在维护上。有一次我发现环境监测节点里用的那个温湿度读取库有个 bug在湿度超过 90% 的时候会返回错误值。我修好了这个库然后需要重新编译环境监测节点的固件、烧录、测试。这没问题。但车库门控制器和鱼缸控制器里也用了同一个库虽然它们目前没暴露这个问题但我知道这个 bug 迟早会咬我一口。于是我又得把另外两个项目的代码拉出来分别编译、分别烧录。这还只是三个项目。如果你手上有十个、二十个设备分布在不同的地方每次改一行公共代码就要全部重新烧录一遍这个工作量是灾难性的。更别说有些设备装在很难够到的地方比如天花板夹层里、室外防水盒里拆下来烧录再装回去半天就没了。传统模式还有一个问题功能耦合。环境监测节点里温湿度采集、OLED 显示、WiFi 上传、OTA 升级这些逻辑全部编译在一个固件里。我想改显示格式得重新编译整个固件我想调整上传间隔也得重新编译。这些功能之间没有清晰的边界改一处可能影响另一处测试成本很高。1.2 “应用平台”思路能带来什么改变应用平台的核心思路是把固件分成两层底层是“系统固件”负责提供硬件抽象、网络连接、文件系统、内存管理等基础能力上层是“应用”每个应用是一个独立的可执行单元通过系统固件提供的接口来访问硬件和网络。这样做的好处很直接。系统固件只需要烧录一次之后所有功能变更都通过安装、更新、卸载应用来完成。公共库的 bug 修复只需要更新系统固件里的那一份所有应用自动受益。每个应用只关心自己的业务逻辑不需要重复实现 WiFi 连接、OTA 升级这些通用功能。打个比方传统模式就像把整个操作系统和应用程序编译成一个 exe 文件改任何东西都要重新编译整个 exe。应用平台模式就像正常的操作系统内核和应用程序分开应用程序可以独立安装和更新。对于 ESP32 来说这个思路还有一个额外的好处内存隔离。每个应用运行在自己的内存空间里一个应用崩溃了不会把整个系统带崩。系统固件可以捕获应用的异常重启这个应用而不是重启整个设备。对于部署在远处的设备来说这个特性太重要了。1.3 哪些场景最适合这种架构不是所有 ESP32 项目都适合做成应用平台。如果你的设备只干一件事而且这辈子都不会改那传统模式更简单、更省资源。但以下几种场景应用平台的优势非常明显。第一种是多功能设备。比如一个中控面板既要显示温湿度又要控制灯光还要能查看摄像头画面。这些功能由不同的应用实现用户可以按需安装。不需要摄像头功能的用户就不装那个应用省内存省 Flash。第二种是需要频繁更新功能的设备。比如一个广告展示屏内容每周都要换。传统模式每次换内容都要重新编译烧录应用平台模式下只需要推送一个新的展示应用上去。第三种是多设备统一管理的场景。你有一百个设备硬件相同但用途不同。传统模式要维护一百份固件应用平台模式只需要一份系统固件加一百个应用配置。第四种是需要第三方扩展的场景。你做了一个硬件产品希望别人能给你写插件。应用平台提供了标准的应用接口第三方开发者只需要按照接口规范写应用就行不需要了解底层硬件细节。2. 技术选型的纠结为什么最后选了 WebAssembly2.1 几种可选方案的对比确定了要做应用平台之后下一个问题就是应用用什么形式来承载我调研了几种方案每种都试了一下最后选了 WebAssembly。先说说其他几种方案为什么没选。方案一Lua 脚本。Lua 在嵌入式领域很常见NodeMCU 固件就是基于 Lua 的。优点是解释执行、不需要编译、脚本体积小。缺点是性能差而且 Lua 脚本能访问的内存和硬件资源很难做隔离。一个 Lua 脚本死循环了整个系统就卡死了。另外 Lua 的生态在嵌入式领域虽然有一些积累但跟 WebAssembly 比起来还是差很多。方案二MicroPython。MicroPython 在 ESP32 上跑得很成熟语法友好开发效率高。但问题跟 Lua 类似性能一般内存隔离困难而且 MicroPython 固件本身就占了不少 Flash 和 RAM。如果系统固件里再跑一个 MicroPython 解释器留给应用的空间就更少了。方案三ELF 动态加载。把应用编译成 ELF 格式的可执行文件系统固件在运行时加载并执行。这个方案性能最好因为应用是原生代码。但实现难度极大需要自己实现动态链接器、符号解析、内存重定位。而且 ESP32 是 Xtensa 或 RISC-V 架构ELF 加载器需要处理架构相关的细节。最要命的是安全性原生代码可以访问任意内存地址一个恶意或有 bug 的应用可以直接读写系统固件的内存没有任何隔离。方案四WebAssembly。WASM 最初是为浏览器设计的但现在在嵌入式领域也越来越受关注。它的几个特性正好契合应用平台的需求沙箱隔离、跨平台、多种语言支持、体积小。缺点是需要在 ESP32 上跑一个 WASM 运行时这个运行时本身会占用一定的资源。2.2 WebAssembly 在嵌入式场景下的独特优势WASM 最吸引我的一点是沙箱隔离。WASM 应用运行在一个受控的执行环境中它只能访问运行时显式暴露给它的内存和接口。它不能直接读写系统内存不能直接调用硬件寄存器所有对外部世界的访问都必须通过导入函数import function来完成。这意味着系统固件可以精确控制每个应用能做什么。比如一个温度显示应用系统固件只给它暴露“读取温度”和“更新显示”两个接口它就没有办法去控制继电器或者访问网络。这种细粒度的权限控制在 Lua 和 MicroPython 方案里是很难做到的。第二个优势是跨平台。WASM 是平台无关的字节码同一个 WASM 应用可以在 ESP32、ESP32-S3、甚至其他支持 WASM 的芯片上运行。如果以后我想把应用平台移植到别的硬件上应用层代码完全不用改。第三个优势是语言无关。WASM 可以从 C、C、Rust、Zig、AssemblyScript 等多种语言编译而来。这意味着第三方开发者可以用自己熟悉的语言来写应用不需要学习新的脚本语言。第四个优势是体积小。一个简单的 WASM 应用编译出来可能只有几 KB 到几十 KB比 MicroPython 脚本大不了多少但性能要好得多。2.3 实测资源占用WASM 运行时到底吃多少选 WASM 之前我最担心的就是资源占用。ESP32 的 RAM 只有 520KB 左右Flash 通常 4MB要跑一个 WASM 运行时还要留空间给应用和系统功能到底够不够我实测了几种 WASM 运行时。WAMRWebAssembly Micro Runtime是 Intel 开源的专门为嵌入式场景设计有“fast interpreter”和“classic interpreter”两种模式。fast interpreter 模式性能更好但代码体积更大classic interpreter 模式体积小但性能差一些。在 ESP32 上WAMR 的 classic interpreter 模式编译出来大约占 200KB Flash运行时堆内存占用可以控制在 64KB 以内。Wasm3 是另一个选择号称是最快的 WASM 解释器。它的代码体积更小核心运行时大约 100KB Flash但它的内存管理模型跟 WAMR 不太一样需要仔细配置才能跟 ESP-IDF 的内存分配器配合好。我最后选了 WAMR原因是它的文档更完善API 更清晰而且它对“应用生命周期管理”的支持更好。WAMR 提供了 wasm_runtime_instantiate、wasm_runtime_call_wasm_a 等接口可以方便地创建应用实例、调用应用函数、销毁实例。这些接口正好是应用平台需要的。实际跑起来之后系统固件包含 WAMR 运行时、WiFi 协议栈、文件系统、OTA 模块编译出来大约 1.2MB Flash启动后 RAM 占用约 180KB。剩下的 RAM 和 Flash 都可以给应用使用。一个典型的温度显示应用WASM 文件大约 8KB运行时内存占用约 16KB。这意味着同时跑三四个应用完全没问题。3. 系统固件的架构设计把“操作系统”该做的事做对3.1 分层架构的划分逻辑系统固件的架构我改了好几版最后定下来的分层是这样的最底层是ESP-IDF 和硬件驱动这部分不用多说就是 ESP32 的标准开发框架和外设驱动。往上一层是系统服务层包括 WiFi 管理、文件系统我用的是 SPIFFS、OTA 升级、日志系统、电源管理。这些服务对所有应用都是通用的不需要每个应用自己实现。再往上是WASM 运行时层也就是 WAMR。这一层负责加载 WASM 应用、管理应用的生命周期、提供应用与系统服务之间的桥梁。最上面是应用管理层负责应用的安装、卸载、启动、停止、权限管理。这一层是应用平台的核心也是我自己写代码最多的地方。应用管理层对外提供两种接口一种是给用户用的比如通过串口命令或者 Web 界面来安装、启动、停止应用另一种是给应用用的也就是 WASM 导入函数应用通过这些函数来访问系统服务。3.2 应用生命周期管理从安装到卸载的完整链路一个应用从安装到卸载中间要经过好几个状态。我把这些状态和转换逻辑理清楚之后整个应用管理层的代码就清晰了很多。安装用户把 WASM 文件上传到设备上应用管理层会先校验文件格式检查 WASM 魔数、版本号、导入函数列表。如果这个应用需要的导入函数系统没有提供安装就会失败并给出明确的错误信息。校验通过后WASM 文件被保存到文件系统的 /apps 目录下同时生成一个元数据文件记录应用名称、版本、权限、入口函数等信息。启动应用管理层从文件系统读取 WASM 文件调用 WAMR 的接口创建运行时实例。在实例化过程中WAMR 会解析 WASM 模块的导入段把系统固件提供的导入函数绑定进去。实例化成功后应用管理层调用应用的入口函数通常是 _start 或 main应用开始运行。运行应用运行期间系统固件可以通过定时器或者事件来调用应用的导出函数。比如一个显示应用可能导出 refresh 函数系统固件每隔一秒调用一次让应用更新显示内容。应用也可以通过导入函数主动向系统请求服务比如读取传感器数据、发送网络请求。停止当用户请求停止应用或者应用运行超时、发生异常时应用管理层会调用 WAMR 的销毁接口释放应用占用的内存和资源。如果应用在运行期间打开了文件或者网络连接系统固件会在销毁前强制关闭这些资源防止泄漏。卸载停止应用后删除文件系统中的 WASM 文件和元数据文件。如果应用有持久化数据比如配置文件也会一并删除。这套生命周期管理听起来简单但实际实现的时候有很多细节要注意。比如应用启动失败怎么办我的做法是记录失败次数连续失败三次就自动禁用这个应用防止它反复启动失败拖垮系统。再比如应用运行超时怎么处理WAMR 支持执行超时中断我设置了一个 5 秒的超时超过就强制终止应用并记录日志。3.3 导入函数的设计应用能做什么不能做什么导入函数是应用与系统之间的唯一通道设计得好不好直接决定了整个平台的安全性和易用性。我按照“最小权限”原则来设计每个导入函数只做一件事而且只暴露必要的信息。目前系统固件提供的导入函数分为几类日志类app_log_info、app_log_warn、app_log_error。应用通过这些函数输出日志日志会带上应用名称和时间戳方便调试。传感器类sensor_read_temperature、sensor_read_humidity、sensor_read_light。每个函数返回一个浮点数如果传感器不存在或读取失败返回 NaN。执行器类gpio_set_level、gpio_get_level、pwm_set_duty。应用可以通过这些函数控制 GPIO 和 PWM但只能操作系统固件预先分配给这个应用的引脚。应用不能随意指定引脚号只能使用系统固件在元数据中声明的引脚。网络类http_get、http_post、mqtt_publish。应用可以通过这些函数发起网络请求但系统固件会限制请求的频率和目标地址。比如一个应用每分钟最多发起 10 次 HTTP 请求只能访问白名单中的域名。存储类kv_get、kv_set、kv_delete。应用可以通过这些函数读写键值对数据保存在文件系统的 /data 目录下每个应用有独立的命名空间不能访问其他应用的数据。显示类display_clear、display_draw_text、display_draw_rect。应用可以通过这些函数在 OLED 或 LCD 上绘制内容。系统固件负责管理显示缓冲区应用绘制的内容会先写入缓冲区然后由系统固件统一刷新到屏幕。这套导入函数的设计原则是应用能做的事情系统固件都能精确控制应用不能做的事情它没有任何途径去尝试。比如应用不能直接访问内存地址不能直接操作硬件寄存器不能绕过系统固件发起网络请求。4. 应用开发体验从写代码到装进设备4.1 用 C 写一个 WASM 应用的完整流程虽然 WASM 支持多种语言但我最常用的还是 C因为 ESP32 的生态本身就是 C 的天下而且 C 编译出来的 WASM 体积最小。下面用一个温度显示应用作为例子走一遍完整的开发流程。首先需要安装 WASI SDK这是一个专门用来编译 WASM 的 C 工具链。安装好之后写一个简单的 C 文件#include stdio.h #include math.h // 声明导入函数 __attribute__((import_module(env), import_name(sensor_read_temperature))) float sensor_read_temperature(void); __attribute__((import_module(env), import_name(display_clear))) void display_clear(void); __attribute__((import_module(env), import_name(display_draw_text))) void display_draw_text(int x, int y, const char* text); __attribute__((import_module(env), import_name(app_log_info))) void app_log_info(const char* msg); // 应用入口 __attribute__((export_name(_start))) void _start(void) { app_log_info(Temperature app started); } // 导出刷新函数系统固件会定期调用 __attribute__((export_name(refresh))) void refresh(void) { float temp sensor_read_temperature(); char buf[32]; if (isnan(temp)) { snprintf(buf, sizeof(buf), Sensor error); } else { snprintf(buf, sizeof(buf), Temp: %.1f C, temp); } display_clear(); display_draw_text(0, 0, buf); }编译命令大概是这样的/opt/wasi-sdk/bin/clang --targetwasm32 -nostdlib \ -Wl,--no-entry -Wl,--export-all \ -o temp_app.wasm temp_app.c编译出来的 WASM 文件大概 6KB 左右。然后通过设备的 Web 界面或者串口命令把这个文件上传上去应用管理层会自动完成安装和启动。4.2 应用与系统之间的数据交换应用和系统之间的数据交换主要通过两种方式函数参数/返回值和共享内存。函数参数/返回值是最简单的方式。比如 sensor_read_temperature 返回一个 float应用直接拿到温度值。这种方式适合传递简单的标量数据。对于字符串和数组WASM 的内存模型稍微复杂一些。WASM 应用有自己的线性内存系统固件不能直接访问这块内存。当应用调用 display_draw_text 并传入一个字符串指针时这个指针是 WASM 线性内存中的偏移量。系统固件需要通过 WAMR 提供的接口把这个偏移量转换成实际的内存地址才能读取字符串内容。WAMR 提供了 wasm_runtime_addr_app_to_native 和 wasm_runtime_addr_native_to_app 两个函数来做地址转换。我在系统固件里封装了一个辅助函数专门用来从 WASM 内存中读取字符串char* wasm_read_string(wasm_exec_env_t exec_env, uint32_t app_offset, char* buf, int buf_size) { wasm_module_inst_t module_inst wasm_runtime_get_module_inst(exec_env); char* native_ptr wasm_runtime_addr_app_to_native(module_inst, app_offset); if (native_ptr NULL) { return NULL; } strncpy(buf, native_ptr, buf_size - 1); buf[buf_size - 1] \0; return buf; }这个函数在 display_draw_text 的实现里会用到。应用传入的字符串指针是 WASM 内存偏移量系统固件通过这个函数把它转换成可读的字符串。4.3 调试应用时踩过的坑调试 WASM 应用比调试普通 ESP32 固件要麻烦一些因为应用运行在沙箱里不能直接用 GDB 附加。我踩过的坑主要有这几个。第一个坑是日志输出不完整。应用调用 app_log_info 输出日志但有时候日志只输出了一半就没了。后来发现是因为 WASM 应用传入的字符串指针指向的内存区域在系统固件读取之前就被应用修改了。解决办法是在导入函数的实现里先把字符串拷贝到系统固件的缓冲区再做后续处理。第二个坑是浮点数精度问题。WASM 的浮点数是 IEEE 754 标准但 ESP32 的 FPU 在某些情况下会有精度差异。我遇到过温度值在 WASM 里算出来是 25.3但传到系统固件变成 25.299999 的情况。解决办法是在显示之前做四舍五入或者在应用里用整数运算代替浮点运算。第三个坑是内存不足。WASM 应用默认的线性内存大小是 64KB如果应用里用了大数组或者递归调用很容易超出。WAMR 支持在实例化时指定更大的内存但 ESP32 的 RAM 有限不能随便加大。我的做法是在应用元数据里声明需要的内存大小应用管理层在启动应用前检查剩余内存是否足够不够就拒绝启动并给出提示。5. 实测效果与性能数据5.1 应用启动速度和运行开销我实测了几个典型应用的启动速度和运行开销。测试平台是 ESP32-WROOM-32240MHz 主频4MB Flash520KB RAM。应用名称WASM 体积启动时间运行内存CPU 占用温度显示6KB45ms16KB2%网络时钟12KB68ms24KB5%MQTT 上报18KB82ms32KB8%简单游戏35KB120ms48KB15%启动时间是从应用管理层发出启动命令到应用入口函数执行完毕的时间。运行内存是应用实例占用的 WASM 线性内存加上 WAMR 的管理开销。CPU 占用是在应用空闲时的测量值实际运行时会有波动。从数据来看启动速度完全可以接受最快的应用 45ms 就起来了最慢的也就 120ms。运行内存方面一个应用平均占用 20-30KB同时跑三四个应用大概需要 100KB 左右的 RAM对于 520KB 的 ESP32 来说完全够用。5.2 与传统固件模式的对比为了量化应用平台的优势我做了一个对比测试。同一个功能温湿度采集 OLED 显示 WiFi 上传分别用传统固件模式和应用平台模式实现对比几个关键指标。对比项传统固件模式应用平台模式首次开发时间4 小时6 小时含系统固件修改显示格式重新编译烧录15 分钟更新应用30 秒修改上传间隔重新编译烧录15 分钟更新应用30 秒修复公共库 bug重新编译烧录15 分钟更新系统固件2 分钟新增一个功能修改固件30 分钟安装新应用1 分钟Flash 占用1.1MB1.2MB系统 8KB应用RAM 占用150KB180KB系统 16KB应用从数据可以看出应用平台模式在首次开发时多花了 2 小时主要是系统固件的开发但后续每次功能变更都能节省大量时间。特别是修改显示格式和上传间隔这种小改动传统模式要 15 分钟应用平台模式只要 30 秒。如果设备装在难以够到的地方这个时间差距会更明显。5.3 稳定性与异常处理实测稳定性是我最关心的指标。应用平台模式的一个核心优势是隔离性一个应用崩溃不会影响其他应用和系统固件。我做了几个破坏性测试来验证这一点。测试一让一个应用进入死循环。系统固件在 5 秒后检测到超时强制终止了这个应用其他应用和系统功能正常。日志里记录了“App xxx timeout, killed”。测试二让一个应用尝试访问非法内存地址。WAMR 的内存访问检查捕获了这个操作应用被终止系统固件记录了“App xxx memory access violation”。测试三让一个应用疯狂申请内存。WAMR 的内存分配器在达到应用内存上限后拒绝分配应用收到分配失败的错误可以选择优雅退出或者被系统强制终止。测试四让一个应用在运行期间突然断电。重新上电后系统固件检测到上次运行状态异常自动将应用恢复到停止状态不会自动启动等待用户手动确认。这几个测试跑下来系统固件本身没有崩溃过其他应用也没有受到影响。这说明 WASM 的沙箱隔离确实起到了作用。6. 踩坑记录那些让我熬夜的瞬间6.1 WAMR 内存配置的坑WAMR 在 ESP32 上跑内存配置是最容易出问题的地方。WAMR 需要一块连续的内存作为 WASM 应用的线性内存池这块内存的大小和分配方式直接影响能跑多少应用。我一开始用的是 WAMR 的“内存池”模式预先分配一块 128KB 的内存作为所有应用的共享内存池。这个模式的好处是内存利用率高多个应用可以共享这块内存。但问题是如果一个应用申请了大量内存不释放其他应用就没内存可用了。后来我改成了“每个应用独立内存”模式每个应用在实例化时分配自己的线性内存应用销毁时释放。这个模式的内存隔离性更好但内存碎片化更严重。ESP32 的 RAM 本来就紧张跑几个应用之后碎片化就很明显了再想启动新应用就可能因为找不到连续内存而失败。最后的解决方案是混合模式系统启动时预留一块 96KB 的内存作为应用内存池每个应用从这个池子里分配内存。应用销毁时内存归还到池子里但池子会定期做碎片整理。这个方案兼顾了隔离性和内存利用率实测下来同时跑四个应用没有问题。6.2 导入函数绑定失败的排查过程导入函数绑定失败是我遇到的最难排查的问题之一。现象是应用安装成功但启动时失败日志里只有一句“instantiate failed”没有任何详细信息。我一开始以为是 WASM 文件的问题换了好几个应用都是一样的结果。后来把 WAMR 的日志级别调到 debug才看到真正的错误信息“import function env.sensor_read_temperature not found”。原来是我在系统固件里注册导入函数的时候模块名写成了“sensors”而不是“env”。WASM 应用里声明的导入模块名是“env”系统固件注册的模块名是“sensors”两边对不上WAMR 就找不到对应的函数。这个问题的教训是WAMR 的错误信息默认不够详细排查导入函数相关的问题时一定要把日志级别调到 debug。另外系统固件和应用的导入函数声明必须严格一致包括模块名、函数名、参数类型、返回类型任何一项不匹配都会导致绑定失败。6.3 应用更新时的版本兼容问题应用平台支持应用更新但更新过程中有一个版本兼容问题。假设系统固件是 v1.0提供了 10 个导入函数。一个应用是基于 v1.0 开发的使用了其中 8 个。后来系统固件升级到 v1.1删除了一个不常用的导入函数或者修改了某个导入函数的参数类型。这个应用在 v1.1 的系统固件上就无法启动了。解决这个问题有两种思路。一种是严格向后兼容系统固件永远不删除或修改已有的导入函数只增加新的。另一种是在应用元数据里声明依赖的系统固件版本应用管理层在启动应用前检查版本是否匹配。我采用了第二种思路因为第一种思路会限制系统固件的演进。应用元数据里有一个 min_system_version 字段应用管理层在启动应用前会比较这个字段和当前系统固件版本。如果不匹配拒绝启动并提示用户更新应用或系统固件。7. 这套方案还能怎么扩展7.1 应用商店的雏形目前应用的安装是通过 Web 界面手动上传 WASM 文件。如果要做成一个真正的应用平台下一步自然是做一个应用商店。设备可以定期从服务器拉取应用列表用户通过 Web 界面浏览和安装应用。应用商店的核心是应用元数据的标准化。每个应用需要一个清单文件包含应用名称、版本、作者、描述、权限、依赖、截图等信息。设备端根据清单文件来展示应用信息和检查兼容性。这个方向我已经在做了目前有一个简单的应用仓库存放了几个示例应用的 WASM 文件和清单文件。设备可以通过 HTTP 请求获取应用列表然后选择安装。7.2 多应用协同目前的应用是相互独立的一个应用不能直接调用另一个应用的函数。但在实际场景中应用之间可能需要协同。比如一个传感器应用采集数据一个显示应用展示数据一个上传应用把数据发到服务器。这三个应用需要共享数据。我的做法是通过系统固件提供的键值存储来实现应用间通信。传感器应用把数据写入 kv_set(temperature, value)显示应用和上传应用通过 kv_get(temperature) 读取数据。这种方式简单可靠但实时性一般适合对延迟不敏感的场景。如果需要更实时的通信可以考虑在系统固件里实现一个消息总线应用可以订阅和发布消息。这个功能我还在设计中主要考虑的是消息格式和权限控制。7.3 支持更多语言目前我主要用 C 来写 WASM 应用但 WASM 支持的语言远不止 C。Rust 编译到 WASM 的体验很好体积也控制得不错。AssemblyScript 是 TypeScript 的子集对于前端开发者来说上手很快。Zig 也是一个不错的选择编译速度快生成的 WASM 体积小。我试过用 Rust 写了一个简单的应用编译出来的 WASM 比 C 版本大了约 30%但开发体验好很多特别是错误处理和内存安全方面。如果应用逻辑比较复杂用 Rust 可能比 C 更合适。支持更多语言的关键是提供统一的导入函数绑定。不同语言编译到 WASM 时导入函数的声明方式可能不同。比如 C 用attribute((import_module))Rust 用 extern C 加 #[link(wasm_import_module env)]。系统固件不需要关心应用是用什么语言写的只需要保证导入函数的模块名、函数名、签名一致就行。7.4 安全加固的方向目前的应用平台在安全性方面还有一些可以加固的地方。比如 WASM 应用虽然不能直接访问系统内存但可以通过导入函数发起网络请求。如果一个恶意应用利用 http_post 把敏感数据发到外部服务器系统固件目前只能通过域名白名单来限制但白名单本身也可能被绕过。下一步我打算在系统固件里增加更细粒度的网络访问控制比如限制每个应用每天的网络请求次数、限制请求的数据大小、对请求内容做敏感信息过滤。另外应用的 WASM 文件在安装时可以做签名验证确保应用来自可信的开发者没有被篡改。这些安全加固措施会增加系统固件的复杂度但对于一个要长期运行、可能部署在不可控环境中的设备来说这些投入是值得的。8. 给想尝试这套方案的人一些实在建议如果你看完上面的内容也想在 ESP32 上搞一个类似的应用平台我有几个建议可以帮你少走弯路。先从简单的开始。不要一上来就搞完整的应用生命周期管理、权限控制、应用商店。先做一个最小可用的版本系统固件能加载一个 WASM 文件能调用它的入口函数能通过导入函数点个灯。把这个跑通了再逐步增加功能。WAMR 的版本选择很重要。WAMR 的更新比较频繁不同版本之间的 API 可能有变化。建议选一个稳定的 release 版本不要用 main 分支的最新代码。我目前用的是 WAMR 1.3.x 系列在 ESP32 上跑得比较稳。内存管理要提前规划。ESP32 的 RAM 是稀缺资源WASM 运行时的内存开销、应用的内存需求、系统固件自身的内存占用这些都要提前算清楚。建议在系统启动时就预留好应用内存池不要等到运行时再动态分配否则很容易碎片化。导入函数的设计要克制。不要因为方便就把所有系统功能都暴露给应用。每多一个导入函数就多一个安全风险点。只暴露应用真正需要的功能而且要对每个导入函数的参数做严格校验。日志和错误处理要做好。WASM 应用的调试比普通固件麻烦没有好的日志很难定位问题。建议在系统固件里实现一个统一的日志系统应用通过导入函数输出日志日志带上应用名称、时间戳、日志级别方便过滤和排查。测试要充分。应用平台的稳定性依赖于系统固件的健壮性。在正式部署之前一定要做充分的异常测试应用死循环、应用内存溢出、应用非法内存访问、应用启动失败、应用更新失败这些场景都要覆盖到。这套方案我目前已经在几个设备上跑了几个月整体稳定性还不错。最让我满意的是更新应用的便捷性以前改一行代码要拆设备、烧固件、装回去现在只需要在 Web 界面上传一个新的 WASM 文件几秒钟就完成了。对于我这种经常改需求的人来说这个效率提升是实实在在的。
返回列表