手搓JSVM第 11 篇:打包、模块与 CLI:把 VM 产物变成可运行文件

0 阅读3分钟

手搓JSVM第 11 篇:打包、模块与 CLI:把 VM 产物变成可运行文件

11-cover.png

源码仓库

1. 本文目标

前面几篇已经把编译器和 runtime 的核心部件搭起来了。接下来要解决的是工程化闭环:

如何把 bytecode、constantPool、函数元信息和 runtime 打包成一个可运行 JS 文件?

本篇覆盖:

  1. ProgramArtifact

  2. Runtime Source

  3. Pack

  4. IIFE / CJS / ESM 包装

  5. Module Bundling

  6. CLI

  7. Debug Info

2. 为什么需要打包

如果只生成 bytecode:

[4, 0, 0, 19, 0]

这还只是编译数据,用户不能把它当作完整程序直接运行。

要真正执行,还需要 runtime:

function __jsvmpRun(metadata, globalObject) { ... }

同时还要把 runtime 所需的 metadata 一并带上:

{
  bytecode,
  constantPool,
  functions
}

形象化比喻:游戏卡带

bytecode 像游戏关卡数据,runtime 像游戏机,metadata 像说明书。三者单独存在都不构成最终交付物;打包就是把它们装进一张卡带,让用户拿到后可以直接运行。

11-pack-cartridge.png

3. 前置知识

概念说明
Artifact编译产物数据
Runtime执行 bytecode 的解释器
Pack把 runtime 与 artifact 拼成 JS
IIFE立即执行函数表达式
CJSCommonJS 输出
ESMES Module 输出
CLI命令行入口

4. 源码中的对应位置

概念源码位置说明
compile()src/compiler/pipeline.ts主编译流程
ProgramArtifactsrc/compiler/types.tsbytecode 与元数据结构
generateRuntimeSource()src/compiler/runtime-gen.ts生成运行时源码
packArtifact()src/compiler/pack.ts打包 runtime + metadata
bundle()src/compiler/bundler.ts模块打包
CLIsrc/cli.ts命令行参数与入口

5. 核心数据结构

ProgramArtifact

它解决什么问题

把 runtime 执行程序时需要的 bytecode、常量和函数信息集中保存。

11-program-artifact.png

教学版简化实现
const artifact = {
  format: 'iife',
  bytecode: [],
  constantPool: [],
  functions: [],
  entryFunctionId: 0,
};

Runtime Source

它解决什么问题

提供真正读取并解释 bytecode 的执行函数。

教学版简化实现
function generateRuntimeSource() {
  return `function __run(metadata){ return 0 }`;
}

Packed Output

它解决什么问题

把 runtime 与 artifact 合到一起,让用户得到一个可直接运行的 JS 文件。

教学版简化实现
const output = `
(function(){
  const metadata = ${JSON.stringify(artifact)};
  ${runtimeSource}
  return __run(metadata);
})()
`;

6. Mermaid 图解

flowchart LR
    A["Source File"] --> B["compile()"]
    B --> C["ProgramArtifact"]
    C --> D["Runtime Source"]
    D --> E["packArtifact()"]
    C --> E
    E --> F["Output JS"]

11-pack-pipeline.png

flowchart TD
    A["CLI input"] --> B["resolve format"]
    B --> C{"bundle?"}
    C -->|yes| D["bundle modules"]
    C -->|no| E["compile single file"]
    D --> F["parse/lower/emit"]
    E --> F
    F --> G["pack"]
    G --> H["write output"]

11-cli-flow.png

7. 伪代码

function compile(inputFile, outputFile, options):
    source = read inputFile
    format = resolveFormat(source)

    if bundle enabled:
        source = bundle(source)

    ast = parse(source)
    ir = lower(ast)
    bytecode = emit(ir)
    runtime = generateRuntimeSource()
    code = pack(runtime, bytecode)

    write outputFile
    return code

8. 教学版实现代码

function packArtifact(artifact) {
  const metadata = JSON.stringify(artifact);

  return `
(function () {
  function __miniRun(metadata) {
    const code = metadata.bytecode;
    const pool = metadata.constantPool;
    const regs = [];
    let pc = 0;

    while (pc < code.length) {
      const op = code[pc++];
      if (op === 4) regs[code[pc++]] = pool[code[pc++]];
      else if (op === 19) return regs[code[pc++]];
      else throw new Error('unknown opcode: ' + op);
    }
  }

  return __miniRun(${metadata});
})()
`.trim();
}

9. 示例输入与输出

Artifact

const artifact = {
  bytecode: [4, 0, 0, 19, 0],
  constantPool: [42],
};

Packed Output

const code = packArtifact(artifact);
console.log(eval(code)); // 42

10. 执行过程拆解

Step阶段产物
1parse/lower/emitProgramArtifact
2generate runtime__jsvmpRun 源码
3pack自包含 JS 文件
4execute outputVM runtime 执行 bytecode
5return输出结果

11. 与原始源码的差异

主题教学版正式源码
Runtime极简同步执行支持 sync/async/generator
Format只演示 IIFE支持 IIFE/ESM/CJS
Module不支持支持 ESM/CJS bundling
Global简化注入 __vmp_global、require/module/exports
Debug不支持可包含 debugInfo

12. 常见问题

Q1:为什么 output 仍然是 JS?

因为这个项目做的是 JS virtualization:源码先变成 bytecode,再由自定义 JS runtime 解释,所以最终产物仍然需要交给 JS 引擎运行。

Q2:打包是不是混淆?

不是。打包只是把 runtime 和 metadata 放到一起。混淆属于额外阶段。

13. 本文小结

到这里,从“编译数据”到“可运行产物”的工程闭环就完整了。

下一篇将继续讲:

第 12 篇:完整最小 JSVM 实现与源码设计复盘