这次不手写轮子,能用现成最好的库就用。先说清楚选型逻辑,再给完整代码,最后给开源全流程。

  ---
  一、先纠正一个关键认知:哪些部分有"最好的库",哪些没有

  一个"字节码虚拟机 + 编译器"项目分四块,市面上库的覆盖情况是这样的:

  ┌────────────────────┬────────┬───────────────────────────────────────┬──────────────────────────────────────────┐
  │        模块        │ 占工作 │         有没有现成最好的库?          │                   选型                   │
  │                    │   量   │                                       │                                          │
  ├────────────────────┼────────┼───────────────────────────────────────┼──────────────────────────────────────────┤
  │ 词法+语法分析(前  │ ~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/elsewhile、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. forbreak/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
  上也能直接跑。需要我把这套代码落成真实文件并跑通测试时,随时说一声。

更多推荐