用最新的Swoole做一个PHP原生的字节码虚拟机,类似一个迷你JVM。加上与之配套的编译器
·
这次不手写轮子,能用现成最好的库就用。先说清楚选型逻辑,再给完整代码,最后给开源全流程。
---
一、先纠正一个关键认知:哪些部分有"最好的库",哪些没有
一个"字节码虚拟机 + 编译器"项目分四块,市面上库的覆盖情况是这样的:
┌────────────────────┬────────┬───────────────────────────────────────┬──────────────────────────────────────────┐
│ 模块 │ 占工作 │ 有没有现成最好的库? │ 选型 │
│ │ 量 │ │ │
├────────────────────┼────────┼───────────────────────────────────────┼──────────────────────────────────────────┤
│ 词法+语法分析(前 │ ~60% │ 有,而且是顶级的 │ nikic/php-parser(PHPStan、Rector、PHP-C │
│ 端) │ │ │ S-Fixer 全用它) │
├────────────────────┼────────┼───────────────────────────────────────┼──────────────────────────────────────────┤
│ 字节码 + │ ~30% │ 没有——这就是你项目存在的意义,谁也替│ 自己写(约 300 行) │
│ VM(后端) │ │ 你写不了 │ │
├────────────────────┼────────┼───────────────────────────────────────┼──────────────────────────────────────────┤
│ CLI 命令行 │ ~5% │ 有 │ symfony/console(事实标准) │
├────────────────────┼────────┼───────────────────────────────────────┼──────────────────────────────────────────┤
│ 网络/并发 │ ~5% │ 有 │ Swoole 6.x(最新大版本,协程 + 线程) │
└────────────────────┴────────┴───────────────────────────────────────┴──────────────────────────────────────────┘
最佳方式的精髓在这一招:别发明新语言、别手写词法/语法分析器。直接让用户写 PHP 的安全子集,用 nikic/php-parser 把 PHP
解析成 AST(这是全 PHP 生态最强的解析器,零 bug 等级),你只写"AST →字节码"的编译后端和 VM。这样:
- 最难、最容易出 bug 的 60%(词法/语法/转义/优先级/报错定位)全部白嫖业界最强实现;
- 用户不用学新语言,写的就是 PHP,但跑在你的沙箱 VM 里——这才是产品级卖点;
- 你的代码量集中在最有教学价值、最像 JVM 的部分:指令集、操作数栈、调用帧。
为什么不用其他库?(选型否决记录,README 里也该写)
- symfony/expression-language:只支持单行表达式,没有语句/循环/函数定义,做不成 VM;
- doctrine/lexer:只是个词法分析骨架,语法分析还得自己写,等于半自研;
- ANTLR4 PHP runtime:要装 Java 工具链生成解析器,重得离谱,社区几乎没人维护 PHP target。
所以结论:前端 php-parser、CLI symfony/console、并发 Swoole 6、质量 PHPUnit 11 + PHPStan 2(level max)+
PHP-CS-Fixer——全是各自领域的最好库。V本体 300 行,是项目的全部灵魂。
---
二、做这个的作用是什么?(大白话,30 秒版)
1. 沙箱执行用户代码:低代码平台、运营规则、工作流脚本,用户写 PHP 你不敢 eval()(等于交出服务器),跑在你的 VM
里就只能用你白名单里的能力,还能限制执行步数防死循环。
2. 吃透语言底层:OPcode、常量池、操作数栈、调用帧——PH的 Zend 引擎和 JVM 都是这套东西,你亲手实现一遍,八股变实战。
3. Swoole 的化学反应:编译一次(贵)→字节码缓存在常驻内存(这就是你自己的 OPcache)→
每个请求一个协程执行(便宜),脚本里 sleep() 是协程级的不阻塞别人。这是 PHP-FPM 模式做不到的。
4. 开源履历:有编译器、VM、测试、CI 的项目,含金量碾压 CRUD。
---
三、整体架构(大白话)
用户提交的 PHP 子集代码
│
▼
nikic/php-parser ──→AST(语法树,库帮你建好,带行号)
│
▼
Compiler(你写的,~200行)──→Chunk(字节码 + 常量池,就像 .class 文件)
│ │
│ 缓存在 Swoole 常驻内存(你的 OPcache)
▼ │
VM(你写的,~300行)◄──────────────┘
栈式虚拟机:操作数栈 + 调用帧,和 JVM 同构
每个 HTTP 请求一个 Swoole 协程跑一个 VM 实例
支持的 PHP 子集:变量、四则运算、.
拼接、比较、&&/||(短路)、if/elseif/else、while、function/return/递归、echo、白名单内置函数(strlen、sleep
等)。白名单之外的任何语法(include、exec、new、$$var……)编译期直接拒绝——这就是沙箱的安全模型:默认拒绝,显式放行 。
---
四、完整代码
▎ 真实项目里一个类一个文件,路径都在注释里标了。环境:PHP ≥8.2,Swoole ≥6.0。注意 Swoole 不支持原生
▎ Windows,你这台机器用 WSL2 或 Docker 跑(文末给了 Docker 命令);纯 VM 部分(不含 serve)Windows 也能跑。
4.1 composer.json(最好的库都在这)
{
"name": "yourname/php-sandbox-vm",
"description": "A bytecode VM for a safe PHP subset. Frontend by nikic/php-parser, coroutine runtime by Swoole 6.
A mini-JVM you can read in an afternoon.",
"type": "library",
"license": "MIT",
"keywords": ["vm", "bytecode", "sandbox", "swoole", "compiler", "php-parser"],
"require": {
"php": ">=8.2",
"nikic/php-parser": "^5.4",
"symfony/console": "^7.2"
},
"require-dev": {
"phpunit/phpunit": "^11.5",
"phpstan/phpstan": "^2.1",
"friendsofphp/php-cs-fixer": "^3.68",
"swoole/ide-helper": "^6.0"
},
"suggest": {
"ext-swoole": ">=6.0, for the coroutine execution server and non-blocking sleep()"
},
"autoload": { "psr-4": { "SandboxVM\\": "src/" } },
"autoload-dev": { "psr-4": { "SandboxVM\\Tests\\": "tests/" } },
"bin": ["bin/svm"],
"scripts": {
"test": "phpunit tests --colors=always",
"stan": "phpstan analyse src --level=max",
"fix": "php-cs-fixer fix"
}
}
4.2 错误类 + 指令集 + Chunk(字节码容器)
大白话:Op 是指令表,每条指令一个数字编号——JV的 iadd/invokestatic 也就是这么回事。Chunk
是"一段编译产物":指令流(纯数字数组)+ 常量池(数字、字符串、编译好的函数都放这,和 .class 文件的常量池一个概念)+
行号表(报错定位)。disassemble() 是反汇编器,相当于 javap -c,学习调试全靠它。
<?php
// ========== src/VmError.php ==========
declare(strict_types=1);
namespace SandboxVM;
final class VmError extends \Exception
{
public function __construct(string $message, public readonly int $srcLine = 0)
{
parent::__construct($srcLine > 0 ? "[第 {$srcLine} 行] {$message}" : $message);
}
}
// ========== src/Bytecode/Op.php ==========
namespace SandboxVM\Bytecode;
final class Op
{
public const CONSTANT = 1; // [常量池下标] 压一个常量进栈
public const NIL = 2; // 压 null
public const POP = 3; // 弹掉栈顶(语句的结果没人要)
public const ADD = 4; public const SUB = 5;
public const MUL = 6; public const DIV = 7;
public const MOD = 8;
public const CONCAT = 9; // PHP 的 . 拼接
public const EQ = 10; public const NEQ = 11; // == !=
public const ID = 12; public const NID = 13; // === !==
public const LT = 14; public const GT = 15;
public const LTE = 16; public const GTE = 17;
public const NOT = 18; public const NEG = 19;
public const JMP = 20; // [目标地址] 无条件跳
public const JMP_IF_F = 21; // [目标地址] 弹栈顶,是假就跳(if/while/&& 全靠它)
public const LOAD = 22; // [名字下标] 读变量
public const STORE = 23; // [名字下标] 写变量(不弹栈:赋值是表达式)
public const DEF_FN = 24; // [常量下标] 注册一个函数
public const CALL = 25; // [参数个数] 调函数(函数名已在栈里)
public const RET = 26;
public const PRINT = 27; // echo
public const NAMES = [
1=>'CONSTANT',2=>'NIL',3=>'POP',4=>'ADD',5=>'SUB',6=>'MUL',7=>'DIV',
8=>'MOD',9=>'CONCAT',10=>'EQ',11=>'NEQ',12=>'ID',13=>'NID',14=>'LT',
15=>'GT',16=>'LTE',17=>'GTE',18=>'NOT',19=>'NEG',20=>'JMP',
21=>'JMP_IF_F',22=>'LOAD',23=>'STORE',24=>'DEF_FN',25=>'CALL',
26=>'RET',27=>'PRINT',
];
public const HAS_OPERAND = [
self::CONSTANT, self::JMP, self::JMP_IF_F,
self::LOAD, self::STORE, self::DEF_FN, self::CALL,
];
}
// ========== src/Bytecode/Chunk.php ==========
final class Chunk
{
/** @var int[] */
public array $code = [];
/** @var mixed[] */
public array $constants = [];
/** @var int[] */
public array $lines = [];
public function emit(int $op, ?int $operand = null, int $line = 0): int
{
$this->code[] = $op;
$this->lines[] = $line;
if ($operand !== null) {
$this->code[] = $operand;
$this->lines[] = $line;
}
return count($this->code) - 1; // 最后写入的位置,跳转打补丁用
}
public function addConstant(mixed $value): int
{
foreach ($this->constants as $i => $c) {
if ($c === $value) return $i; // 常量去重
}
$this->constants[] = $value;
return count($this->constants) - 1;
}
/** 反汇编,相当于 javap -c */
public function disassemble(string $title = 'main'): string
{
$out = "== {$title} ==\n";
$i = 0;
while ($i < count($this->code)) {
$op = $this->code[$i];
$name = Op::NAMES[$op] ?? "?{$op}";
if (in_array($op, Op::HAS_OPERAND, true)) {
$operand = $this->code[$i + 1];
$c = $this->constants[$operand] ?? null;
$hint = in_array($op, [Op::CONSTANT, Op::LOAD, Op::STORE, Op::DEF_FN], true)
? ' ; ' . (is_string($c) ? "\"{$c}\"" : (is_scalar($c) ? (string) $c : get_debug_type($c)))
: '';
$out .= sprintf("%04d %-10s %d%s\n", $i, $name, $operand, $hint);
$i += 2;
} else {
$out .= sprintf("%04d %s\n", $i, $name);
$i++;
}
}
foreach ($this->constants as $c) {
if ($c instanceof \SandboxVM\Runtime\VmFunction) {
$out .= $c->chunk->disassemble("function {$c->name}");
}
}
return $out;
}
}
4.3 编译器(核心差异点:AST 来自 nikic/php-parser,不再手写前端)
大白话:ParserFactory 一行代码就把 PHP 源码变成带行号的语法树——转义、优先级、错误恢复这些苦活全是库干的。我们只做一 件
事:递归走树,每个节点吐几条字节码。match 的 default 分支就是沙箱大门:不认识的节点一律抛错,白名单之外全部拒绝。
唯一的技术亮点是 if/while 的"回填"(backpatching):编译条件跳转时还不知道要跳到哪(后面代码没编译呢),先吐 JMP_IF_F
0xFFFF 占位,编译完目标代码再回头把地址填上。所有编译器都这么干。
<?php
// ========== src/Compiler/Compiler.php ==========
declare(strict_types=1);
namespace SandboxVM\Compiler;
use PhpParser\Error as ParseError;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;
use PhpParser\ParserFactory;
use SandboxVM\Bytecode\Chunk;
use SandboxVM\Bytecode\Op;
use SandboxVM\Runtime\VmFunction;
use SandboxVM\VmError;
final class Compiler
{
private Chunk $chunk;
public function compileSource(string $source): Chunk
{
// 用户可以不写 <?php,我们帮补上
$code = str_starts_with(ltrim($source), '<?php')
? $source
: "<?php\n" . $source;
// ===== 这一行顶替了 1000 行手写词法+语法分析器 =====
$parser = (new ParserFactory())->createForNewestSupportedVersion();
try {
$ast = $parser->parse($code) ?? [];
} catch (ParseError $e) {
throw new VmError('语法错误: ' . $e->getRawMessage(), $e->getStartLine());
}
$this->chunk = new Chunk();
foreach ($ast as $stmt) {
$this->stmt($stmt);
}
$this->chunk->emit(Op::NIL);
$this->chunk->emit(Op::RET); // 主程序末尾:返回 null →VM 停机
return $this->chunk;
}
// ================= 语句 =================
private function stmt(Node $n): void
{
$line = $n->getStartLine();
match (true) {
$n instanceof Stmt\Expression => $this->exprStmt($n, $line),
$n instanceof Stmt\Echo_ => $this->echoStmt($n, $line),
$n instanceof Stmt\If_ => $this->ifStmt($n, $line),
$n instanceof Stmt\While_ => $this->whileStmt($n, $line),
$n instanceof Stmt\Function_ => $this->fnDecl($n, $line),
$n instanceof Stmt\Return_ => $this->returnStmt($n, $line),
$n instanceof Stmt\Block => $this->block($n->stmts),
$n instanceof Stmt\Nop => null, // 空语句(多余的分号)
// ===== 沙箱大门:白名单之外的语法一律拒绝 =====
default => throw new VmError(
'沙箱限制: 不支持的语法 ' . $n->getType()
. '(仅支持 变量/echo/if/while/function/return 与白名单函数)',
$line
),
};
}
/** @param Stmt[] $stmts */
private function block(array $stmts): void
{
foreach ($stmts as $s) {
$this->stmt($s);
}
}
private function exprStmt(Stmt\Expression $n, int $line): void
{
$this->expr($n->expr);
$this->chunk->emit(Op::POP, null, $line); // 结果没人要,弹掉保持栈平衡
}
private function echoStmt(Stmt\Echo_ $n, int $line): void
{
foreach ($n->exprs as $e) {
$this->expr($e);
$this->chunk->emit(Op::PRINT, null, $line);
}
}
private function ifStmt(Stmt\If_ $n, int $line): void
{
// php-parser 把 elseif 单独给出来,我们编译成链式跳转
$endJumps = [];
$this->expr($n->cond);
$nextBranch = $this->emitJump(Op::JMP_IF_F, $line); // 假 →去下一个分支
$this->block($n->stmts);
$endJumps[] = $this->emitJump(Op::JMP, $line); // 真分支完 →跳到最后
$this->patchJump($nextBranch);
foreach ($n->elseifs as $elseif) {
$this->expr($elseif->cond);
$nextBranch = $this->emitJump(Op::JMP_IF_F, $elseif->getStartLine());
$this->block($elseif->stmts);
$endJumps[] = $this->emitJump(Op::JMP, $elseif->getStartLine());
$this->patchJump($nextBranch);
}
if ($n->else !== null) {
$this->block($n->else->stmts);
}
foreach ($endJumps as $j) {
$this->patchJump($j); // 所有分支的"跳到最后"统一回填到这里
}
}
private function whileStmt(Stmt\While_ $n, int $line): void
{
$loopStart = count($this->chunk->code);
$this->expr($n->cond);
$exit = $this->emitJump(Op::JMP_IF_F, $line);
$this->block($n->stmts);
$this->chunk->emit(Op::JMP, $loopStart, $line); // 跳回去再判断
$this->patchJump($exit);
}
private function fnDecl(Stmt\Function_ $n, int $line): void
{
$params = [];
foreach ($n->params as $p) {
if (!$p->var instanceof Expr\Variable || !is_string($p->var->name)) {
throw new VmError('沙箱限制: 不支持的参数写法', $line);
}
$params[] = $p->var->name;
}
// 函数体编译进独立 Chunk ——JVM 里每个方法也有自己的 Code 属性
$sub = new self();
$sub->chunk = new Chunk();
$sub->block($n->stmts);
$sub->chunk->emit(Op::NIL, null, $line); // 没写 return 的兜底
$sub->chunk->emit(Op::RET, null, $line);
$fn = new VmFunction($n->name->toString(), $params, $sub->chunk);
$this->chunk->emit(Op::DEF_FN, $this->chunk->addConstant($fn), $line);
}
private function returnStmt(Stmt\Return_ $n, int $line): void
{
if ($n->expr !== null) {
$this->expr($n->expr);
} else {
$this->chunk->emit(Op::NIL, null, $line);
}
$this->chunk->emit(Op::RET, null, $line);
}
// ================= 表达式 =================
private function expr(Node $n): void
{
$line = $n->getStartLine();
match (true) {
$n instanceof Scalar\Int_,
$n instanceof Scalar\Float_,
$n instanceof Scalar\String_ => $this->chunk->emit(
Op::CONSTANT, $this->chunk->addConstant($n->value), $line
),
$n instanceof Expr\ConstFetch => $this->constFetch($n, $line),
$n instanceof Expr\Variable => $this->variable($n, $line, Op::LOAD),
$n instanceof Expr\Assign => $this->assign($n, $line),
$n instanceof Expr\BinaryOp => $this->binary($n, $line),
$n instanceof Expr\BooleanNot => $this->unary($n->expr, Op::NOT, $line),
$n instanceof Expr\UnaryMinus => $this->unary($n->expr, Op::NEG, $line),
$n instanceof Expr\FuncCall => $this->call($n, $line),
default => throw new VmError(
'沙箱限制: 不支持的表达式 ' . $n->getType(), $line
),
};
}
private function constFetch(Expr\ConstFetch $n, int $line): void
{
$name = strtolower($n->name->toString());
$value = match ($name) {
'true' => true,
'false' => false,
'null' => null,
default => throw new VmError("沙箱限制: 未知常量 {$name}", $line),
};
if ($value === null) {
$this->chunk->emit(Op::NIL, null, $line);
} else {
$this->chunk->emit(Op::CONSTANT, $this->chunk->addConstant($value), $line);
}
}
private function variable(Expr\Variable $n, int $line, int $op): void
{
if (!is_string($n->name)) { // 拦住 $$var 可变变量——沙箱大忌
throw new VmError('沙箱限制: 不支持可变变量 $$x', $line);
}
$this->chunk->emit($op, $this->chunk->addConstant($n->name), $line);
}
private function assign(Expr\Assign $n, int $line): void
{
if (!$n->var instanceof Expr\Variable) {
throw new VmError('沙箱限制: 只能给普通变量赋值', $line);
}
$this->expr($n->expr);
$this->variable($n->var, $line, Op::STORE); // STORE 不弹栈,$a = $b = 1 才能连
}
private function binary(Expr\BinaryOp $n, int $line): void
{
// && 和 || 必须短路(右边可能根本不执行),单独处理
if ($n instanceof Expr\BinaryOp\BooleanAnd) { $this->logicalAnd($n, $line); return; }
if ($n instanceof Expr\BinaryOp\BooleanOr) { $this->logicalOr($n, $line); return; }
$this->expr($n->left);
$this->expr($n->right);
$op = match ($n->getOperatorSigil()) {
'+' => Op::ADD, '-' => Op::SUB, '*' => Op::MUL,
'/' => Op::DIV, '%' => Op::MOD, '.' => Op::CONCAT,
'==' => Op::EQ, '!=' => Op::NEQ, '<>' => Op::NEQ,
'===' => Op::ID, '!==' => Op::NID,
'<' => Op::LT, '>' => Op::GT, '<=' => Op::LTE, '>=' => Op::GTE,
default => throw new VmError(
"沙箱限制: 不支持运算符 '{$n->getOperatorSigil()}'", $line
),
};
$this->chunk->emit($op, null, $line);
}
private function logicalAnd(Expr\BinaryOp\BooleanAnd $n, int $line): void
{
$this->expr($n->left);
$toFalse1 = $this->emitJump(Op::JMP_IF_F, $line); // 左假 →整体假,右边不执行
$this->expr($n->right);
$toFalse2 = $this->emitJump(Op::JMP_IF_F, $line);
$this->emitBool(true, $line);
$end = $this->emitJump(Op::JMP, $line);
$this->patchJump($toFalse1);
$this->patchJump($toFalse2);
$this->emitBool(false, $line);
$this->patchJump($end);
}
private function logicalOr(Expr\BinaryOp\BooleanOr $n, int $line): void
{
$this->expr($n->left);
$this->chunk->emit(Op::NOT, null, $line);
$toTrue1 = $this->emitJump(Op::JMP_IF_F, $line); // 左真 →整体真,右边不执行
$this->expr($n->right);
$this->chunk->emit(Op::NOT, null, $line);
$toTrue2 = $this->emitJump(Op::JMP_IF_F, $line);
$this->emitBool(false, $line);
$end = $this->emitJump(Op::JMP, $line);
$this->patchJump($toTrue1);
$this->patchJump($toTrue2);
$this->emitBool(true, $line);
$this->patchJump($end);
}
private function unary(Node $operand, int $op, int $line): void
{
$this->expr($operand);
$this->chunk->emit($op, null, $line);
}
private function call(Expr\FuncCall $n, int $line): void
{
if (!$n->name instanceof Node\Name) {
throw new VmError('沙箱限制: 不支持动态函数调用 $f()', $line);
}
// 先压函数名,再压参数,最后 CALL n ——由 VM 在运行时解析名字
$nameIdx = $this->chunk->addConstant(strtolower($n->name->toString()));
$this->chunk->emit(Op::CONSTANT, $nameIdx, $line);
foreach ($n->args as $arg) {
if (!$arg instanceof Node\Arg) {
throw new VmError('沙箱限制: 不支持 ... 展开参数', $line);
}
$this->expr($arg->value);
}
$this->chunk->emit(Op::CALL, count($n->args), $line);
}
// ---- 跳转回填 ----
private function emitJump(int $op, int $line): int
{
return $this->chunk->emit($op, 0xFFFF, $line);
}
private function patchJump(int $operandPos): void
{
$this->chunk->code[$operandPos] = count($this->chunk->code);
}
private function emitBool(bool $b, int $line): void
{
$this->chunk->emit(Op::CONSTANT, $this->chunk->addConstant($b), $line);
}
}
4.4 运行时对象 + 内置函数白名单
大白话:VmFunction 是编译好的用户函数(揣着自己的字节码)。NativeRegistry 是沙箱的能力边界——脚本能调用的PHP
函数只有这张表里的,exec、file_get_contents 想都别想。sleep 是关键演示:在 Swoole 协程里走 Coroutine::sleep,让出 CPU
不阻塞别的请求。
<?php
// ========== src/Runtime/VmFunction.php ==========
declare(strict_types=1);
namespace SandboxVM\Runtime;
use SandboxVM\Bytecode\Chunk;
final class VmFunction
{
/** @param string[] $params */
public function __construct(
public readonly string $name,
public readonly array $params,
public readonly Chunk $chunk,
) {}
}
// ========== src/Runtime/NativeRegistry.php ==========
final class NativeRegistry
{
/** @var array<string, \Closure> */
private array $fns = [];
public function __construct()
{
// ===== 默认白名单:每一个都是无害的纯函数 =====
$this->fns = [
'strlen' => fn (array $a) => strlen((string) $a[0]),
'strtoupper' => fn (array $a) => strtoupper((string) $a[0]),
'strtolower' => fn (array $a) => strtolower((string) $a[0]),
'str_repeat' => fn (array $a) => str_repeat((string) $a[0], (int) $a[1]),
'abs' => fn (array $a) => abs($a[0]),
'max' => fn (array $a) => max(...$a),
'min' => fn (array $a) => min(...$a),
'intdiv' => fn (array $a) => intdiv((int) $a[0], (int) $a[1]),
'time' => fn (array $a) => time(),
// 协程感知的 sleep:有 Swoole 协程就非阻塞
'sleep' => function (array $a) {
$sec = (float) ($a[0] ?? 0);
if (extension_loaded('swoole') && \Swoole\Coroutine::getCid() > 0) {
\Swoole\Coroutine::sleep($sec);
} else {
usleep((int) ($sec * 1_000_000));
}
return null;
},
];
}
/** 宿主应用扩展能力的口子,比如注入 getOrderAmount() */
public function register(string $name, \Closure $fn): void
{
$this->fns[strtolower($name)] = $fn;
}
public function get(string $name): ?\Closure
{
return $this->fns[$name] ?? null;
}
}
4.5 VM 本体(项目的心脏,唯一没有库可用的部分)
大白话:一个大 while 循环——取指令、执行、取下一条。函数调用= 压一个"调用帧"(记住该回到哪),return = 弹帧。JVM
的栈帧、程序计数器(ip)就是这俩东西。两道安全闸:步数上限(死循环跑不满一秒就被杀)、输出走注入的闭包(服务模式下每个
请求隔离捕获,VM 永不直接 echo)。
<?php
// ========== src/Vm/Frame.php ==========
declare(strict_types=1);
namespace SandboxVM\Vm;
use SandboxVM\Bytecode\Chunk;
/** 一次调用的"现场":执行到哪了(ip = 程序计数器)+ 自己的局部变量 */
final class Frame
{
public int $ip = 0;
/** @var array<string, mixed> */
public array $locals = [];
public function __construct(public readonly Chunk $chunk) {}
}
// ========== src/Vm/Vm.php ==========
use SandboxVM\Bytecode\Chunk;
use SandboxVM\Bytecode\Op;
use SandboxVM\Runtime\NativeRegistry;
use SandboxVM\Runtime\VmFunction;
use SandboxVM\VmError;
final class Vm
{
/** @var mixed[] 操作数栈:一切计算的中转站 */
private array $stack = [];
/** @var Frame[] 调用栈 */
private array $frames = [];
/** @var array<string, VmFunction> 用户函数表(PHP 的函数和变量本来就分家) */
private array $functions = [];
private int $steps = 0;
private \Closure $stdout;
public function __construct(
?\Closure $stdout = null,
public readonly NativeRegistry $natives = new NativeRegistry(),
public int $maxSteps = 10_000_000,
) {
$this->stdout = $stdout ?? static fn (string $s) => print $s;
}
public function run(Chunk $main): void
{
$this->frames[] = new Frame($main);
while (true) {
$frame = $this->frames[count($this->frames) - 1];
$code = $frame->chunk->code;
$op = $code[$frame->ip++];
$line = $frame->chunk->lines[$frame->ip - 1] ?? 0;
if (++$this->steps > $this->maxSteps) {
throw new VmError(
"执行中止: 超过 {$this->maxSteps} 条指令上限(疑似死循环)", $line
);
}
switch ($op) {
case Op::CONSTANT:
$this->stack[] = $frame->chunk->constants[$code[$frame->ip++]];
break;
case Op::NIL: $this->stack[] = null; break;
case Op::POP: array_pop($this->stack); break;
case Op::ADD: $this->arith(fn ($a, $b) => $a + $b, $line); break;
case Op::SUB: $this->arith(fn ($a, $b) => $a - $b, $line); break;
case Op::MUL: $this->arith(fn ($a, $b) => $a * $b, $line); break;
case Op::MOD: $this->arith(function ($a, $b) use ($line) {
if ($b == 0) throw new VmError('运行时错误: 对 0 取模', $line);
return $a % $b;
}, $line); break;
case Op::DIV: $this->arith(function ($a, $b) use ($line) {
if ($b == 0) throw new VmError('运行时错误: 除以 0', $line);
return is_int($a) && is_int($b) && $a % $b === 0
? intdiv($a, $b) : $a / $b;
}, $line); break;
case Op::CONCAT: {
$b = array_pop($this->stack);
$a = array_pop($this->stack);
$this->stack[] = $this->toStr($a) . $this->toStr($b);
break;
}
case Op::EQ: $this->binop(fn ($a, $b) => $a == $b); break;
case Op::NEQ: $this->binop(fn ($a, $b) => $a != $b); break;
case Op::ID: $this->binop(fn ($a, $b) => $a === $b); break;
case Op::NID: $this->binop(fn ($a, $b) => $a !== $b); break;
case Op::LT: $this->cmp(fn ($a, $b) => $a < $b, $line); break;
case Op::GT: $this->cmp(fn ($a, $b) => $a > $b, $line); break;
case Op::LTE: $this->cmp(fn ($a, $b) => $a <= $b, $line); break;
case Op::GTE: $this->cmp(fn ($a, $b) => $a >= $b, $line); break;
case Op::NOT:
$this->stack[] = !$this->truthy(array_pop($this->stack));
break;
case Op::NEG: {
$v = array_pop($this->stack);
$this->checkNum($v, $line);
$this->stack[] = -$v;
break;
}
case Op::JMP:
$frame->ip = $code[$frame->ip];
break;
case Op::JMP_IF_F: {
$target = $code[$frame->ip++];
if (!$this->truthy(array_pop($this->stack))) {
$frame->ip = $target;
}
break;
}
case Op::LOAD: {
$name = $frame->chunk->constants[$code[$frame->ip++]];
if (!array_key_exists($name, $frame->locals)) {
throw new VmError("运行时错误: 未定义变量 \${$name}", $line);
}
$this->stack[] = $frame->locals[$name];
break;
}
case Op::STORE: {
$name = $frame->chunk->constants[$code[$frame->ip++]];
// peek 不 pop:赋值表达式本身有值
$frame->locals[$name] = $this->stack[count($this->stack) - 1];
break;
}
case Op::DEF_FN: {
/** @var VmFunction $fn */
$fn = $frame->chunk->constants[$code[$frame->ip++]];
$this->functions[strtolower($fn->name)] = $fn;
break;
}
case Op::CALL: {
$argc = $code[$frame->ip++];
$args = $argc > 0 ? array_splice($this->stack, -$argc) : [];
$name = array_pop($this->stack);
if (isset($this->functions[$name])) {
$fn = $this->functions[$name];
if (count($args) !== count($fn->params)) {
throw new VmError(sprintf(
'运行时错误: %s() 需要 %d 个参数,传了 %d 个',
$fn->name, count($fn->params), count($args)
), $line);
}
if (count($this->frames) > 256) {
throw new VmError('运行时错误: 调用栈过深(递归失控?)', $line);
}
$new = new Frame($fn->chunk);
// PHP 语义:函数内只看得见参数,看不见外部变量
foreach ($fn->params as $i => $p) {
$new->locals[$p] = $args[$i];
}
$this->frames[] = $new;
} elseif (($native = $this->natives->get($name)) !== null) {
$this->stack[] = $native($args);
} else {
throw new VmError(
"沙箱限制: 函数 {$name}() 不存在或不在白名单内", $line
);
}
break;
}
case Op::RET: {
$value = array_pop($this->stack);
array_pop($this->frames);
if ($this->frames === []) {
return; // 主程序返回 →停机
}
$this->stack[] = $value; // 返回值交还调用方
break;
}
case Op::PRINT:
($this->stdout)($this->toStr(array_pop($this->stack)));
break;
default:
throw new VmError("VM 内部错误: 非法指令 {$op}", $line);
}
}
}
// ---- 工具 ----
private function arith(\Closure $f, int $line): void
{
$b = array_pop($this->stack);
$a = array_pop($this->stack);
$this->checkNum($a, $line);
$this->checkNum($b, $line);
$this->stack[] = $f($a, $b);
}
private function cmp(\Closure $f, int $line): void
{
$b = array_pop($this->stack);
$a = array_pop($this->stack);
$this->stack[] = $f($a, $b);
}
private function binop(\Closure $f): void
{
$b = array_pop($this->stack);
$a = array_pop($this->stack);
$this->stack[] = $f($a, $b);
}
private function checkNum(mixed $v, int $line): void
{
if (!is_int($v) && !is_float($v)) {
throw new VmError('类型错误: 算术运算需要数字,字符串拼接请用 .', $line);
}
}
private function truthy(mixed $v): bool
{
return (bool) $v; // 沿用 PHP 自己的真值规则,用户零学习成本
}
private function toStr(mixed $v): string
{
return match (true) {
$v === null => '',
$v === true => '1',
$v === false => '', // 和 PHP echo 行为保持一致
is_float($v) => rtrim(rtrim(sprintf('%.10F', $v), '0'), '.'),
default => (string) $v,
};
}
}
4.6 门面 + CLI(symfony/console,最好的命令行库)
<?php
// ========== src/Sandbox.php ==========
declare(strict_types=1);
namespace SandboxVM;
use SandboxVM\Bytecode\Chunk;
use SandboxVM\Compiler\Compiler;
use SandboxVM\Runtime\NativeRegistry;
use SandboxVM\Vm\Vm;
/** 一行用起来:Sandbox::run('echo 1 + 1;') → "2" */
final class Sandbox
{
public static function compile(string $source): Chunk
{
return (new Compiler())->compileSource($source);
}
public static function run(
string $source,
int $maxSteps = 10_000_000,
?NativeRegistry $natives = null,
): string {
$out = '';
$vm = new Vm(
function (string $s) use (&$out) { $out .= $s; },
$natives ?? new NativeRegistry(),
$maxSteps,
);
$vm->run(self::compile($source));
return $out;
}
}
#!/usr/bin/env php
<?php
// ========== bin/svm ==========
declare(strict_types=1);
foreach ([__DIR__ . '/../vendor/autoload.php', __DIR__ . '/../../../autoload.php'] as $f) {
if (file_exists($f)) { require $f; break; }
}
use SandboxVM\Sandbox;
use SandboxVM\VmError;
use SandboxVM\Vm\Vm;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'run', description: '编译并执行一个 PHP 子集脚本')]
final class RunCommand extends Command
{
protected function configure(): void
{
$this->addArgument('file', InputArgument::REQUIRED, '脚本文件路径');
}
protected function execute(InputInterface $in, OutputInterface $out): int
{
$src = @file_get_contents($in->getArgument('file'));
if ($src === false) {
$out->writeln('<error>读不到文件</error>');
return Command::FAILURE;
}
try {
$main = function () use ($src) {
(new Vm())->run(Sandbox::compile($src)); // 默认 stdout 直接打屏
};
// 有 Swoole 就包进协程容器,脚本里的 sleep 自动非阻塞
extension_loaded('swoole') ? \Swoole\Coroutine\run($main) : $main();
return Command::SUCCESS;
} catch (VmError $e) {
$out->writeln('<error>' . $e->getMessage() . '</error>');
return Command::FAILURE;
}
}
}
#[AsCommand(name: 'dump', description: '打印字节码反汇编(相当于 javap -c)')]
final class DumpCommand extends Command
{
protected function configure(): void
{
$this->addArgument('file', InputArgument::REQUIRED);
}
protected function execute(InputInterface $in, OutputInterface $out): int
{
try {
$out->write(
Sandbox::compile(file_get_contents($in->getArgument('file')))->disassemble()
);
return Command::SUCCESS;
} catch (VmError $e) {
$out->writeln('<error>' . $e->getMessage() . '</error>');
return Command::FAILURE;
}
}
}
#[AsCommand(name: 'serve', description: '启动 Swoole 字节码执行服务')]
final class ServeCommand extends Command
{
protected function configure(): void
{
$this->addArgument('port', InputArgument::OPTIONAL, '端口', '9501');
}
protected function execute(InputInterface $in, OutputInterface $out): int
{
require __DIR__ . '/../src/Server/serve.php';
startServer((int) $in->getArgument('port'));
return Command::SUCCESS;
}
}
$app = new Application('svm - PHP Sandbox VM', '0.1.0');
$app->addCommands([new RunCommand(), new DumpCommand(), new ServeCommand()]);
$app->run();
4.7 Swoole 6 执行服务(项目的杀手锏)
大白话:这就是"你自己的 OPcache +
你自己的应用服务器"。同一段源码第二次提交,直接命中内存里的字节码缓存跳过编译;每个请求一个协程,脚本 sleep(3)
时协程挂起,同一个 worker 照样服务其他几千个请求。开两个终端同时 curl 一个带 sleep(3) 的脚本验证:两个都约 3
秒返回,而不是 6 秒。
<?php
// ========== src/Server/serve.php ==========
declare(strict_types=1);
use SandboxVM\Sandbox;
use SandboxVM\VmError;
use SandboxVM\Vm\Vm;
function startServer(int $port = 9501): void
{
if (!extension_loaded('swoole')) {
fwrite(STDERR, "serve 模式需要 Swoole >= 6.0(pecl install swoole)\n");
exit(1);
}
$server = new Swoole\Http\Server('0.0.0.0', $port);
$server->set([
'worker_num' => swoole_cpu_num(),
'enable_coroutine' => true,
'hook_flags' => SWOOLE_HOOK_ALL, // 一键协程化所有阻塞 IO
]);
// 每个 worker 自己的字节码缓存:无锁、够用(这就是你的 OPcache)
$cache = [];
$server->on('start', function () use ($port) {
echo "Sandbox VM 服务已启动: http://0.0.0.0:{$port}\n";
echo "用法: curl -X POST --data-binary @script.php http://127.0.0.1:{$port}/run\n";
});
$server->on('request', function (
Swoole\Http\Request $req,
Swoole\Http\Response $resp
) use (&$cache) {
$resp->header('Content-Type', 'application/json; charset=utf-8');
if ($req->server['request_uri'] !== '/run') {
$resp->status(404);
$resp->end(json_encode(['ok' => false, 'error' => 'POST /run,body 放源码']));
return;
}
$source = $req->getContent() ?: '';
if (trim($source) === '') {
$resp->status(400);
$resp->end(json_encode(['ok' => false, 'error' => '源码为空']));
return;
}
$t0 = microtime(true);
try {
// ①编译(带缓存)
$key = md5($source);
$hit = isset($cache[$key]);
if (!$hit) {
if (count($cache) > 1000) {
$cache = []; // 简单粗暴防内存膨胀
}
$cache[$key] = Sandbox::compile($source);
}
// ②当前协程里执行,输出按请求隔离
$out = '';
$vm = new Vm(
function (string $s) use (&$out) { $out .= $s; },
maxSteps: 5_000_000, // 服务模式收紧沙箱
);
$vm->run($cache[$key]);
$resp->end(json_encode([
'ok' => true,
'output' => $out,
'cache' => $hit ? 'hit' : 'miss',
'timeMs' => round((microtime(true) - $t0) * 1000, 2),
], JSON_UNESCAPED_UNICODE));
} catch (VmError $e) {
$resp->status(422);
$resp->end(json_encode(
['ok' => false, 'error' => $e->getMessage()],
JSON_UNESCAPED_UNICODE
));
} catch (\Throwable) {
$resp->status(500);
$resp->end(json_encode(['ok' => false, 'error' => 'VM 内部错误']));
}
});
$server->start();
}
4.8 测试(PHPUnit 11)
大白话:语言项目的测试是最爽的——给一段源码,断言输出。注意测试样例就是普通PHP,因为我们的源语言就是 PHP
子集。沙箱拒绝项(exec 不在白名单、include 不在语法白名单)也必须测,安全边界是测出来的不是想出来的。
<?php
// ========== tests/EndToEndTest.php ==========
declare(strict_types=1);
namespace SandboxVM\Tests;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use SandboxVM\Sandbox;
use SandboxVM\VmError;
final class EndToEndTest extends TestCase
{
#[DataProvider('programs')]
public function testProgramOutput(string $source, string $expected): void
{
$this->assertSame($expected, Sandbox::run($source));
}
public static function programs(): array
{
return [
'算术优先级' => ['echo 1 + 2 * 3;', '7'],
'字符串拼接' => ['echo "a" . 1 . "\n";', "a1\n"],
'变量' => ['$a = 5; $a = $a + 1; echo $a;', '6'],
'链式赋值' => ['$a = $b = 2; echo $a + $b;', '4'],
'if-elseif' => ['$x = 2; if ($x == 1) { echo "a"; } elseif ($x == 2) { echo "b"; } else { echo "c"; }',
'b'],
'while' => ['$i = 0; while ($i < 3) { echo $i; $i = $i + 1; }', '012'],
'函数与递归' => [
'function fib($n) { if ($n < 2) { return $n; } return fib($n - 1) + fib($n - 2); } echo fib(10);',
'55',
],
'短路与' => ['echo false && boom();', ''], // boom 不存在但不会被调用
'短路或' => ['echo true || boom();', '1'],
'内置白名单' => ['echo strtoupper("hi") . strlen("abc");', 'HI3'],
];
}
public function testDivisionByZero(): void
{
$this->expectException(VmError::class);
Sandbox::run('echo 1 / 0;');
}
public function testInfiniteLoopKilledByStepLimit(): void
{
$this->expectException(VmError::class);
$this->expectExceptionMessageMatches('/指令上限/');
Sandbox::run('while (true) { $x = 1; }', maxSteps: 10_000);
}
public function testDangerousFunctionRejected(): void
{
$this->expectException(VmError::class);
$this->expectExceptionMessageMatches('/白名单/');
Sandbox::run('echo exec("ls");'); // 运行期拦截:不在白名单
}
public function testDangerousSyntaxRejected(): void
{
$this->expectException(VmError::class);
$this->expectExceptionMessageMatches('/沙箱限制/');
Sandbox::run('include "/etc/passwd";'); // 编译期拦截:语法不在白名单
}
public function testHostCanInjectNatives(): void
{
$natives = new \SandboxVM\Runtime\NativeRegistry();
$natives->register('order_amount', fn (array $a) => 350);
$out = Sandbox::run(
'if (order_amount() >= 300) { echo "减50"; }',
natives: $natives
);
$this->assertSame('减50', $out); // ←规则引擎用法,就这么简单
}
}
目录结构总览:
php-sandbox-vm/
├── bin/svm
├── src/
│ ├── Sandbox.php VmError.php
│ ├── Bytecode/ Op.php Chunk.php
│ ├── Compiler/ Compiler.php
│ ├── Runtime/ VmFunction.php NativeRegistry.php
│ ├── Vm/ Vm.php Frame.php
│ └── Server/ serve.php
├── tests/EndToEndTest.php
├── examples/fib.php rules.php
├── composer.json README.md LICENSE CHANGELOG.md CONTRIBUTING.md SECURITY.md
└── .github/workflows/ci.yml
本地(你是 Windows)用 Docker 一条命令进开发环境:
docker run --rm -it -v "$PWD":/app -w /app -p 9501:9501 phpswoole/swoole:php8.3 bash
# 容器里:composer install && composer test && php bin/svm serve
---
五、开源项目全流程:从 0 到持续维护(大白话手册)
阶段 0:动手前(半天)
- 调研差异点:GitHub 上有教学 VM,但"用 php-parser 做前端 + Swoole 协程沙箱服务"这个组合没有,README 第一段就写这个。
- 定 v1 边界:只做上面这个特性集。数组、foreach、闭包全进 ROADMAP。开源项目死因第一名是 v1 贪大。
- 查名:Packagist 搜 php-sandbox-vm 确认没被占。
阶段 1:仓库初始化(1 小时)
mkdir php-sandbox-vm && cd php-sandbox-vm && git init
四件套:
1. LICENSE:MIT(库类项目默认选项,商用友好)。没 LICENSE 别人法律上不能用你的代码,新手最常犯的错。
2. .gitignore:/vendor/、.phpunit.result.cache、.php-cs-fixer.cache、composer.lock(库不提交 lock,应用才提交)。
3. README.md 骨架:一句话定位 + 徽章(CI/Packagist/License)→30 秒上手(composer require + 5 行代码 + 输出)→
为什么比 eval() 安全 →沙箱白名单表 →curl 示例 →Roadmap。
4. CHANGELOG.md:Keep a Changelog 格式,从 ## [Unreleased] 开始记。
阶段 2:开发与质量门禁
- 每个特性一个分支,节奏:先写测试 →实现 →svm dump 肉眼看字节码 →提交。
- Commit 用 Conventional Commits:feat(vm): add CALL/RET and call frames、fix(compiler): reject variable
variables。以后能自动生成 CHANGELOG。
- 三道门禁(composer scripts 已配):composer test(行为)、composer stan(PHPStan level max,类型坑)、composer
fix(PSR-12 风格)。
.php-cs-fixer.php:
<?php
return (new PhpCsFixer\Config())
->setRules([
'@PER-CS' => true,
'declare_strict_types' => true,
'ordered_imports' => true,
'no_unused_imports' => true,
])
->setRiskyAllowed(true)
->setFinder(PhpCsFixer\Finder::create()->in(['src', 'tests']));
阶段 3:CI(机器人替你把关)
大白话:每次 push 或别人提 PR,GitHub 自动开干净机器跑全套检查,你不用手动验。shivammathur/setup-php 这个 action
一行就把 Swoole 扩展装好。
# ========== .github/workflows/ci.yml ==========
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4'] # 多版本矩阵:兼容性是库的生命线
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: swoole
coverage: none
- run: composer install --prefer-dist --no-progress
- run: composer test
- run: composer stan
- run: vendor/bin/php-cs-fixer fix --dry-run --diff
阶段 4:发布 v0.1.0
1. 推 GitHub,填 About 和 topics(php、swoole、sandbox、virtual-machine、bytecode)。
2. SemVer 打 tag(这是你对用户的契约):0.x 孵化期 API 可变;1.0.0 承诺稳定;修 bug
升补丁号、加功能升次版本、破坏兼容(比如改指令编码、收紧白名单)必须升主版本。
git tag -a v0.1.0 -m "First release" && git push origin v0.1.0
3. Packagist:packagist.org →Submit 仓库地址 →装 GitHub webhook,以后打 tag 自动发版。全世界从此 composer require
yourname/php-sandbox-vm。
4. GitHub Release:基于 tag 写亮点 + 破坏性变更。
阶段 5:社区基建(发布一周内补齐)
┌───────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────┐
│ 文件 │ 大白话用途 │
├───────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ CONTRIBUTING.md │ 怎么跑测试、分支命名、PR 必须带测试且 CI 全绿 │
├───────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ .github/ISSUE_TEMPLATE/bug_report.yml │ 强制填:源码、期望输出、实际输出、PHP/Swoole 版本 │
├───────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ .github/PULL_REQUEST_TEMPLATE.md │ 勾选清单:加测试了吗?更新 CHANGELOG 了吗? │
├───────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ CODE_OF_CONDUCT.md │ Contributor Covenant 模板直接用 │
├───────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ SECURITY.md │ 沙箱项目的命门:"发现沙箱逃逸请发邮件私报,别公开发 │
│ │ issue",并承诺响应时限 │
└───────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────┘
阶段 6:推广(没人知道 = 不存在)
- 写一篇《我用 php-parser + Swoole 做了个 PHP 沙箱虚拟机》(掘金/思否/Reddit r/PHP),讲"为什么 eval 危险、VM
怎么解决"比讲用法更抓人,配 svm dump 的反汇编截图。
- 给 awesome-php、awesome-swoole 提 PR 自荐;V2EX、Hacker News(Show HN)各发一帖。
- php-parser 的生态页面、Swoole 社区群里分享——你的项目是这两个库的优秀示范案例,作者和社区都乐意转。
阶段 7:持续维护(最考验人)
- 响应节奏:每周固定时段扫 issue,48 小时内至少回一句"收到"。响应速度决定社区生死。
- 标签分诊:bug / enhancement / wontfix / good first issue(必须维护,这是新贡献者入口,比如"给白名单加
str_pad"就是完美的 first issue)。
- PR 原则:小 PR 快合(squash 保持主干干净);大 PR 没先开 issue 讨论的,礼貌引导拆小。
- 安全响应:沙箱逃逸报告 = 最高优先级,修复后发补丁版 + GitHub Security Advisory。
- 版本跟进:开 Dependabot;php-parser 出 v6、Swoole 出新大版本、PHP 8.5 进 CI 矩阵,各发一个 minor 声明支持。
- 弃用策略:删公开 API 先标 @deprecated 一个 minor 周期,下个 major 才真删。绝不在补丁版本搞破坏。
- 防 burnout:README 写明范围("教学/沙箱 VM,不追求 Zend 级性能"),超纲需求礼貌说"欢迎
fork";项目火了提拔活跃贡献者当 co-maintainer。
---
六、Roadmap(升级路线,每项都是一个漂亮 PR)
1. 数组 + foreach(加 NEW_ARRAY/INDEX_GET/INDEX_SET 指令)
2. for、break/continue(需要"循环跳转地址栈",回填技术进阶)
3. 字节码序列化落盘 .svmc 文件——你自己的.class 文件,跑前免编译
4. 变量编译成整数槽位代替名字查表(JVM 的 local slots,VM 提速数倍)
5. 沙箱内 go() 函数:脚本里直接开 Swoole 协程,变成真正的并发脚本沙箱(独家卖点)
6. 常量折叠:编译期把 1+2 折成 3(编译优化第一课)
---
TLDR
- 最佳方式 = 不写前端:源语言就用 PHP 安全子集,nikic/php-parser(生态最强解析器)负责词法/语法/AST,省掉最难的
60%;CLI 用 symfony/console;并发用 Swoole 6 协程;质量用 PHPUnit 11 + PHPStan max + PHP-CS-Fixer。VM
本体没有库可用——那300 行就是你项目存在的意义。
- 作用:安全跑用户提交的 PHP(替代危险的 eval)、规则引擎、吃透 OPcode/JVM 原理、高含金量开源履历。
- 安全模型:语法白名单(编译期拒绝)+ 函数白名单(运行期拒绝)+ 步数上限(杀死循环)+ 输出隔离。
- 开源流程:边界收窄 →LICENSE/README/CHANGELOG →三门禁 →GitHub Actions 多版本矩阵(一行装 Swoole)→SemVer +
Packagist webhook →社区五件套(沙箱项目 SECURITY.md 是命门)→每周分诊、安全报告最高优、绝不在小版本搞破坏。
提醒:Swoole 不支持原生 Windows,serve 模式请用 WSL2 或上面的 Docker 命令;纯编译+VM 部分在你的 Windows PHP
上也能直接跑。需要我把这套代码落成真实文件并跑通测试时,随时说一声。
更多推荐


所有评论(0)