Flipper Zero 上的 Brainfuck 解释器与编辑器:FlipperZeroBrainfuck 源码解析与实战指南
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
FlipperZeroBrainfuck 是一个运行在 Flipper Zero 上的 Brainfuck 语言解释器与代码编辑器,它允许用户在设备上直接编写、保存并运行.b格式的 Brainfuck 脚本,支持文本输入(,指令)与文本输出(.指令)。本文将以 关联文档 为主体,结合本仓库中的完整源码,讲解该应用的场景架构、虚拟按键编辑器布局、解释器底层实现、缓冲区边界与已知限制,帮助你理解其工作原理并快速上手使用。
项目定位与源码布局
FlipperZeroBrainfuck 位于本仓库 Applications/Official/source-OLDER/xMasterX/brainfuck 目录下,属于旧版官方应用源码归档(source-OLDER)中的外部应用(EXTERNAL,即 FAP 插件)。源码由一个非常精简的 C 工程构成:
- brainfuck.c:应用入口与资源分配/释放;
- brainfuck_i.h:核心数据结构与全部缓冲区常量定义;
- worker.c:Brainfuck 解释器本体(8 条指令的执行逻辑);
- views/bf_dev_env.c:设备上的代码编辑环境(虚拟键盘视图);
- scenes/:基于场景管理器(Scene Manager)的 6 个界面场景;
- application.fam:FAP 应用构建元数据。
值得注意的细节来自 brainfuck.c 顶部的注释:作者坦言该应用是"复制 picopass 应用骨架、掏空内部后用其躯壳搭建"出来的,这解释了为什么代码中出现了brainfuck_alloc()、brainfuck_free()等与经典 Flipper 应用模板一致的资源管理模式。
应用元数据与构建方式
application.fam 完整声明了应用的构建信息:
| 字段 | 值 | 说明 |
|---|---|---|
| appid | Brainfuck | 应用唯一标识 |
| apptype | FlipperAppType.EXTERNAL | 外部插件(编译为 .fap 文件) |
| entry_point | brainfuck_app | 入口函数,见 brainfuck.c |
| requires | storage,gui | 依赖 SD 卡存储与 GUI 子系统 |
| stack_size | 8 * 1024(8 KB) | 任务栈大小 |
| fap_icon | bfico.png | 应用图标 |
| fap_category | Misc_Extra | 在应用菜单中的分类 |
| fap_icon_assets | icons | 附带图标资源目录 |
由于它是 EXTERNAL 类型应用,你可以像构建其他 FAP 插件一样,使用 Flipper 固件的 fbt/ufbt 构建链将其编译为.fap插件文件,再拷贝到 Flipper Zero 的 SD 卡上运行。
应用启动与整体架构
入口函数brainfuck_app()的流程(brainfuck.c)如下:
- 调用
brainfuck_alloc()分配BFApp结构体,初始化 ViewDispatcher 与 SceneManager,注册全部视图(Submenu、Popup、TextInput、TextBox、开发环境视图); - 打开
RECORD_GUI与RECORD_NOTIFICATION系统服务记录; - 打开
RECORD_STORAGE,调用storage_simply_mkdir(storage, "/ext/brainfuck")确保/ext/brainfuck目录存在——这是所有脚本文件的存放目录; - 切换到起始场景并进入
view_dispatcher_run()主循环。
BFApp结构体(brainfuck_i.h)中与解释执行直接相关的成员是:
int dataSize; char dataBuffer[BF_INST_BUFFER_SIZE]; // 指令缓冲区,2048 字节 char inputBuffer[BF_INPUT_BUFFER_SIZE]; // 输入缓冲区,64 字节应用的全部界面由 brainfuck_scene_config.h 声明的 6 个场景驱动,其跳转关系与职责如下:
| 场景 | 文件名 | 职责 |
|---|---|---|
| Start | brainfuck_scene_start.c | 主菜单:New / Open / About |
| FileCreate | brainfuck_scene_file_create.c | 输入新脚本名并创建文件 |
| FileSelect | brainfuck_scene_file_select.c | 通过系统文件浏览器选择.b脚本 |
| DevEnv | brainfuck_scene_dev.c | 开发环境:编辑、输入、运行、保存 |
| ExecEnv | brainfuck_scene_exec.c | 执行结果展示(TextBox 视图) |
| SetInput | brainfuck_scene_set_input.c | 编辑,指令的输入缓冲区 |
文件系统约定:/ext/brainfuck 与 .b 脚本
脚本统一存放在 SD 卡/ext/brainfuck目录下,扩展名为.b:
- 新建脚本(brainfuck_scene_file_create.c):进入 TextInput 场景,输入脚本名后拼接为
/ext/brainfuck/<name>.b,先删除旧文件再以FSAM_WRITE + FSOM_CREATE_ALWAYS写入一个空字节,随后直接进入开发环境; - 打开脚本(brainfuck_scene_file_select.c):使用系统
DialogsApp文件浏览器,browser_options限定扩展名.b、基础路径为/ext/brainfuck且不隐藏扩展名; - 加载与清洗(views/bf_dev_env.c):进入开发环境时清空指令缓冲区、以只读方式打开文件读入缓冲区,然后用
strchr(bfChars, ...)把所有非 Brainfuck 指令字符(换行、注释等)统一替换为下划线_,再定位缓冲区末尾作为编辑起点。这意味着注释与换行会被静默清洗掉,加载后缓冲区中只剩 8 个有效指令字符与下划线占位。
开发环境(Dev Environment):12 键虚拟键盘
开发环境是 views/bf_dev_env.c 实现的自绘视图,屏幕顶部用 3 行 × 24 字符的文本框显示当前代码(超出 72 字符时只显示末尾 72 字符,即dbOffset偏移逻辑),下方是两组虚拟按键:
第一排:8 个指令按键(屏幕坐标 x = 1, 17, 33, 49, 65, 81, 97, 113,y = 36)
| 按键序号 | 字符 | 含义 |
|---|---|---|
| 0 | + | 当前单元格加 1 |
| 1 | - | 当前单元格减 1 |
| 2 | < | 数据指针左移 |
| 3 | > | 数据指针右移 |
| 4 | [ | 循环开始 |
| 5 | ] | 循环结束 |
| 6 | . | 输出当前单元格字节 |
| 7 | , | 从输入缓冲区读一个字节 |
第二排:4 个功能按键(使用 icons 目录下的图标绘制,y = 52)
| 按键序号 | 功能 | 触发行为 |
|---|---|---|
| 8 | Backspace | 删除缓冲区最后一个字符并置 0(bf_dev_env.c) |
| 9 | Input | 切换到 SetInput 场景编辑输入缓冲区 |
| 10 | Run | 启动RUNNING...倒计时后执行解释器 |
| 11 | Save | 删除旧文件并以FSAM_WRITE + FSOM_CREATE_ALWAYS写回当前缓冲区 |
按键间的上下左右跳转由buttonMappings[12]表驱动(bf_dev_env.c),结构为{up, down, left, right}。从源码可归纳出导航规律:4 个功能键的上下方向分别映射到其正上方的 2 个指令键(如 Backspace 上下对应+/-,Input 上下对应</>,Run 上下对应[/],Save 上下对应./,),左右方向则在同一排内循环移动。所有指令按键在dataSize < BF_INST_BUFFER_SIZE时向dataBuffer尾部追加对应字符,实现连续输入。
解释器核心实现:worker.c 逐指令拆解
解释器主体在 worker.c,采用全局状态机,运行状态status三态:0空闲、1运行中、2失败(worker.c)。数据模型如下:
- 指令流:
inst指向app->dataBuffer,instCount为指令数,instPtr为当前指令指针; - 数据栈:
bfStack为uint8_t数组(一字节单元格,符合标准 Brainfuck 语义),初始容量BF_STACK_INITIAL_SIZE = 128字节,stackPtr为当前指针; - 输出缓冲区:
wOutput容量BF_OUTPUT_SIZE = 512字节; - 输入缓冲区:
wInput指向app->inputBuffer,最多 64 字节。
主循环beginWorker()(worker.c)从instPtr = 0开始,以指令流中的0x00作为程序结束标记,对 8 个有效字符执行 switch 分派,其余字符(如下划线)直接忽略。每执行一条指令instPtr++并做一次指针合法性校验。
各指令的执行逻辑与边界行为:
| 指令 | 函数 | 行为与边界 |
|---|---|---|
> | rShift() | stackPtr++;越界检查失败置status=2;当指针超过当前容量时以realloc按BF_STACK_STEP_SIZE = 32字节扩容并清零新区域,同时更新stackSizeReal记录实际用到的最大深度 |
< | lShift() | stackPtr--;stackPtr < 0时置status=2 |
+/- | inc()/dec() | 当前单元格自增/自减,uint8_t天然支持 0xFF→0x00 的循环回绕 |
. | print() | 将当前单元格字节写入输出缓冲区;wOutputPtr > BF_OUTPUT_SIZE - 1时回绕到 0(输出环形覆盖) |
, | input() | 从输入缓冲区取字节写入当前单元格;遇到0x00或指针到达 64 时输入指针回绕到 0(worker.c) |
[ | loop() | 当前单元格为 0 时向后跳过到匹配的];不为 0 则继续顺序执行(进入循环体) |
] | endLoop() | 当前单元格不为 0 时向前回跳到匹配的[;为 0 则跳出循环 |
循环匹配(worker.c)通过计数嵌套括号实现:loop()向后扫描时遇[计数加 1、遇]计数减 1,直到计数归零即为匹配位置;endLoop()同理反向扫描。循环跳转与指令指针越界都会触发status = 2的失败终止。
所有操作统一经过runOpCount计数(可通过getOpCount()获取执行步数),且整个解释过程是同步执行的——从 bf_dev_env.c 的绘制回调可以看到,按 Run 后先绘制 3 帧RUNNING...,随后直接调用initWorker()+beginWorker(),把workerGetOutput()写入 TextBox 并切换到执行结果场景。正因为同步阻塞式执行,长时间运行的程序会卡住整个 UI——这正是 README 强调"程序必须终止"的根本原因。
关键常量汇总
brainfuck_i.h 定义了全部关键尺寸,是理解该应用能力边界的基础:
| 宏 | 值 | 含义 |
|---|---|---|
BF_INST_BUFFER_SIZE | 2048 | 指令缓冲区容量(单脚本最大字节数) |
BF_OUTPUT_SIZE | 512 | 输出缓冲区容量,超出后循环覆盖 |
BF_STACK_INITIAL_SIZE | 128 | 数据栈初始容量(字节) |
BF_STACK_STEP_SIZE | 32 | 数据栈每次扩容步长 |
BF_INPUT_BUFFER_SIZE | 64 | ,指令的输入缓冲区容量 |
已知限制与使用注意事项
原文档明确列出了该应用的三大限制,结合源码可进一步确认其成因:
- 程序必须终止,否则冻结在 "RUNNING":由于解释器在 UI 绘制回调中同步执行(见上文),一个死循环程序会无限占用主循环,界面停留在
RUNNING...状态且无法响应按键。README 明确警告 "Major limitation is that programs MUST terminate, or it will freeze at RUNNING"。 - 对死循环无防护:
loop()/endLoop()没有任何循环次数上限或超时机制,[/]不匹配的语法错误同样不会在运行前被检测。 - 对语法错误无防护:虽然 views/bf_dev_env.c 会在加载文件时把非法字符替换为下划线,但括号配对、指针越界(
<移出栈底、>扩容失败)等逻辑性错误只有在运行期才会通过status = 2暴露——越界会直接失败退出,而括号不匹配或死循环则可能造成冻结。
实操建议:编写脚本时保持括号成对、避免无界循环;若程序需要较长运行时间,请先在 PC 端模拟器验证逻辑后再上机。
实战:在设备上编写并运行第一个脚本
下面以一个在标准 Brainfuck 中广泛使用的经典示例("Hello World!",106 字节,远小于 2048 字节的指令缓冲区上限)演示完整流程:
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.操作步骤:
- 启动应用,在主菜单选择New,输入脚本名(如
hello),应用会创建/ext/brainfuck/hello.b并进入开发环境; - 在虚拟键盘上依次录入上述指令:先录入
+、>、[、]等指令键完成代码输入,期间可用 Backspace 删除误输入; - 按Input键进入
Edit input buffer界面,为,指令准备输入数据(本例无输入需求可留空); - 按Run键,屏幕短暂显示
RUNNING...后切换到执行结果视图,.,>等指令的输出会在 TextBox 中显示 "Hello World!"; - 如需保存代码,按Save键,屏幕会显示
SAVED提示,代码写入/ext/brainfuck/hello.b。
一个更简单的可验证示例是输出大写字母H(ASCII 72 = 9 × 8),利用嵌套循环将第二个单元格累加到 72 后输出:
+++++++++[>++++++++<-]>.其执行轨迹为:先让单元格 0 从 0 累加到 9;进入循环后每轮把单元格 1 加 8 并把单元格 0 减 1;循环 9 轮后单元格 1 = 72,指针右移后用.输出字符H。你可以在 worker.c 中逐步对照验证这一过程。
相关资源索引
- 应用自述文档:README.md
- 入口与资源管理:brainfuck.c
- 常量与数据结构定义:brainfuck_i.h
- 解释器实现:worker.c
- 编辑器视图实现:views/bf_dev_env.c
- 场景注册表:scenes/brainfuck_scene_config.h
- 构建元数据:application.fam
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考