当我们谈论浏览器端的 CPU 仿真时,通常会想到使用 JavaScript 编写解释器或 JIT 编译器。然而,x86CSS 项目彻底颠覆了这一认知 —— 它完全不依赖 JavaScript 执行引擎,而是在 CSS 渲染管道的求值过程中完成 x86 指令的模拟。这种实现方式将浏览器的样式计算能力推向了图灵完备的极限,其工程实现细节值得深入分析。
核心设计理念:把 CPU 状态编码为 DOM 与 CSS 属性
x86CSS 的核心思路是将整个计算机的状态映射到浏览器可表达的数据结构中。传统的 CPU 仿真器在内存中维护寄存器组、标志位、程序计数器和 RAM 数据,而 x86CSS 则将这些全部转化为 CSS 自定义属性(Custom Properties)和 DOM 元素的样式状态。每一个「时钟周期」本质上是浏览器对样式规则的重新求值过程,通过触发布局重算来推进虚拟 CPU 的执行。
具体而言,寄存器被定义为顶层容器上的数值型自定义属性,例如 --ax、--bx、--cx、--dx 以及标志位 --zf(零标志)、--cf(进位标志)等。这些属性通过 @property 声明为数值类型,从而让浏览器能够将其作为整数进行数学运算和比较操作。程序计数器 PC 同样是这样一个自定义属性,它决定了当前「活跃」的指令行 —— 只有匹配当前 PC 值的那些 CSS 规则才会被应用,从而实现指令的顺序执行和跳转控制。
内存系统的实现则更为巧妙。整个虚拟 RAM 被建模为一组网格状的 DOM 元素,每个单元格对应一个字节或字。单元格的地址和内容分别存储在元素的属性中,而「当前正在访问的内存单元」则通过容器查询(Container Queries)来选定。当 CPU 要读写某个地址时,容器查询的样式条件会比较 --addr 与单元格的实际索引,只有匹配的单元格才会接收相应的样式更新。这种机制模拟了真实的内存寻址过程,只是将硬件级的地址解码替换成了 CSS 选择器的模式匹配。
指令解码与微操作:选择器充当微代码 ROM
在传统 CPU 中,指令解码器将机器码翻译成一系列微操作;在 x86CSS 中,这个角色由 CSS 选择器承担。每一条 x86 指令都对应着一组 CSS 规则,这些规则带有复杂的选择器条件,例如「当 PC 等于 N 且操作码等于 X 时,执行以下属性更新」。由于 CSS 选择器支持 :has()、:nth-child() 以及容器查询的样式条件,理论上可以表达任意复杂的解码逻辑。
更关键的是,多周期指令被分解为多个微步骤。每个微步骤由额外的状态位控制,例如一个微程序计数器或相位标志。在一个时钟周期内,只有与当前微状态匹配的子规则才会生效;这些规则会更新主状态位,使得下一个微步骤的条件在下一轮布局计算时被满足。通过这种方式,复杂的 x86 指令(如乘除法、字符串操作)被拆解为一系列简单的属性赋值,层层递进完成整个操作。
算术逻辑单元(ALU)的实现同样依赖选择器构建的真值表。每一个位运算(加法、减法、与、或、移位)都被编码为大量的条件规则:当输入满足特定模式时,输出被设置为相应的值。虽然这种方法产生的 CSS 规则数量极其庞大,但它本质上是在用选择器实现一个查表电路 —— 浏览器的高度优化的样式引擎负责快速查找和应用这些规则。
控制流与分支:条件求值驱动跳转
条件跳转是 CPU 执行中最关键的控制流操作。在 x86CSS 中,条件跳转指令通过读取 CSS 标志位属性并使用 if() 函数计算下一条 PC 值来实现。例如,--pc-next: if(var(--zf), <目标地址>, calc(var(--pc) + 1)); 这条规则表达了「如果零标志被设置,则跳转到目标地址,否则继续执行下一条指令」。浏览器的 if() 函数在计算该属性时会评估条件表达式,从而在样式求值阶段完成分支决策。
这种设计的精妙之处在于,分支的「Taken/Not-Taken」结果直接体现为哪个容器查询分支变得有效。当条件满足时,不同的 --pc 值会被写入,而这一变化会在下一次布局计算时选中不同的指令行作为活跃状态,从而实现控制流的转移。调用和返回指令则将一段 DOM 区域建模为栈结构,SP 寄存器作为数值属性决定当前栈顶元素的位置,每一次 push/pop 操作都在对应的 DOM 元素上应用或移除相关样式。
时钟驱动与执行模型:无循环的推进机制
传统仿真器通常在 JavaScript 中维护一个主循环,不断读取 - 解码 - 执行指令。x86CSS 完全不同 —— 它根本没有显式的执行循环。执行的推进完全依赖于浏览器自身的重新布局机制。一个关键的「时钟」属性(或相位位)在每一轮布局中切换其值,这个属性被包含在大量的选择器条件中,因此每次样式重算都会导致不同的微状态规则被激活。
项目中实际使用了两种时钟方案。第一种是纯 CSS 方案,利用动画(Animation)持续改变某个属性的值,强制浏览器不断重新计算样式。第二种是添加极少量 JavaScript 来提供更稳定的时钟信号 —— 但创造者明确指出这是可选的性能优化,完整的纯 CSS 版本仍然可以工作,只是速度较慢且不够稳定。这两种方案的取舍反映了工程实践中「理论可行」与「用户体验」之间的平衡。
输入输出与外设交互:内存映射的实现
键盘输入和屏幕输出通过内存映射 I/O 的方式实现。「设备寄存器」同样是自定义属性,当用户在页面上按下按键时,对应元素的样式状态发生变化,进而改变 CPU 能读取到的属性值。程序通过执行普通的内存读取指令来「轮询」这些设备状态,如同真实硬件上的轮询驱动。这种将用户交互纳入 CSS 求值链条的设计,使得整个仿真系统形成了闭合的反馈回路。
屏幕输出则是对「视频内存」区域的直接渲染。字符数据存储在特定的内存单元格中,而这些单元格的样式直接控制着屏幕上的可见字符。浏览器负责将样式计算的结果渲染为实际的文本或图形,整个过程对上层程序透明。
工程实践的启示
x86CSS 项目揭示了一个重要事实:现代浏览器的 CSS 引擎已经足够强大,能够在特定约束下执行通用计算。其工程实现依赖于几个关键技术的组合:自定义属性提供可计算的状态存储、容器查询提供条件求值能力、if() 函数实现运行时决策、选择器模式匹配完成指令解码。然而,这种方法也有明显的局限 —— 每一条新增指令都需要编写大量的选择器规则,调试困难,性能天花板明显。
对于系统开发者而言,x86CSS 提供了一个独特的视角:当我们可以自由支配渲染管道的求值过程时,「程序」的定义可以远超传统命令序列。选择器的匹配、属性的计算、布局的触发 —— 这些浏览器日常执行的操作,本质上都是一种计算过程。理解这一点,有助于我们在更广阔的工程场景中发现新的可能性。
资料来源:x86CSS 项目主页(https://lyra.horse/x86css/)