Spine Runtimes 跨语言移植工作流程详解

badlogic 发布于 2025-07-06 阅读 10

本文档详细描述了Spine Runtimes的移植程序,用于将Java参考实现中的变更协同移植到目标运行时(如C++)。内容包括porting-plan.json的格式说明、使用vs-claude工具打开文件和查看差异、通过jq命令监控移植进度、使用脚本阅读Java类型和差异分析,以及编译测试方法。文档还给出了完整的移植工作流程:一次性设置(读取元数据、检查约定文件)、逐个类型移植(查找待移植类型、打开文件、用户确认、分析变更、实施移植、用户确认、更新状态和笔记)。目标是确保目标运行时与参考实现功能等价,API精确匹配,实现方法除语言习惯差异外完全相同。

工具

VS Claude

在移植过程中,使用 vs-claude MCP 服务器工具为用户打开文件和差异对比。

// 一次打开多个文件(批量操作)
mcp__vs-claude__open([
  {"type": "file", "path": "/abs/path/Animation.java"},    // Java 源文件
  {"type": "file", "path": "/abs/path/Animation.h"},       // C++ 头文件
  {"type": "file", "path": "/abs/path/Animation.cpp"}      // C++ 源文件
]);

// 打开单个文件并指定行范围
mcp__vs-claude__open({"type": "file", "path": "/abs/path/Animation.java", "startLine": 100, "endLine": 120});

// 查看文件的 git diff
mcp__vs-claude__open({"type": "gitDiff", "path": "/abs/path/Animation.cpp", "from": "HEAD", "to": "working"});

进度跟踪

使用以下 jq 命令监控移植进度:

## 获取总体进度百分比
jq -r '.portingOrder | map(.types[]) | "\(([.[] | select(.portingState == "done")] | length)) types ported out of \(length) total (\(([.[] | select(.portingState == "done")] | length) * 100 / length | floor)% complete)"' porting-plan.json

## 按状态统计类型数量
jq -r '.portingOrder | map(.types[]) | group_by(.portingState) | map({state: .[0].portingState, count: length}) | sort_by(.state)' porting-plan.json

## 列出所有已完成的类型
jq -r '.portingOrder | map(.types[] | select(.portingState == "done") | .name) | sort | join(", ")' porting-plan.json

## 查找剩余的待移植类型
jq -r '.portingOrder | map(.types[] | select(.portingState == "pending") | .name) | length' porting-plan.json

读取 Java 类型

从当前版本中提取某个类型的源代码:

./read-java-type.js <type-name>

## 示例:
./read-java-type.js Property

返回该类型的源代码,每行前面带有行号:

  • 保留精确缩进(包括制表符)
  • 内部类定义被移除(在输出末尾替换为计数)
  • 末尾包含已排除内部类的摘要

类型差异分析

获取显示某个类型变更的内联 diff:

./read-java-type-diff.js <type-name>

## 示例:
./read-java-type-diff.js Property

返回仅针对指定类型的聚焦 diff:

  • 只显示该类型本身的变更(排除内部类变更)
  • + 前缀表示新增行
  • - 前缀表示删除行
  • 单个空格前缀表示未变更行
  • 无行号
  • 末尾包含已排除内部类的摘要

编译测试

对于 C++,在移植过程中测试编译单个文件:

./compile-cpp.js /path/to/spine-cpp/spine-cpp/src/spine/Animation.cpp

对于其他语言,我们无法编译单个文件,不应尝试。

工作流程

一次移植一个类型。确保目标运行时实现与参考实现功能等价。API 必须匹配,除习惯性差异(包括类型名、字段名、方法名、枚举名、参数名等)外。方法的实现必须 完全 匹配,除习惯性差异(如集合类型的差异)外。

按以下步骤移植每个类型:

1. 设置(一次性步骤)

此阶段请勿使用 TodoWrite 和 TodoRead 工具!

  1. 从 porting-plan.json 读取元数据:

    jq '.metadata' porting-plan.json
    
    • 如果失败,则中止并告知用户运行 generate-porting-plan.js
    • 存储以下值供后续使用:
      • targetRuntime(例如 "spine-cpp")
      • targetRuntimePath(例如 "/path/to/spine-cpp/spine-cpp")
      • targetRuntimeLanguage(例如 "cpp")
  2. 并行执行: a. 检查约定文件:

    • 完整读取 ${targetRuntime}-conventions.md(来自步骤 1)。
    • 如果文件缺失:
      • 并行使用 Task 代理分析 targetRuntimePath(来自步骤 1)
      • 记录所有编码模式和约定:
        • 类/接口/枚举定义语法
        • 成员变量命名(前缀如 m_、_ 等)
        • 方法命名约定(camelCase 与 snake_case)
        • 继承语法
        • 文件组织(单个文件与头文件/实现文件分离)
        • 命名空间/模块/包结构
        • 内存管理(GC、手动、智能指针)
        • 错误处理(异常、错误码、Result 类型)
        • 文档格式(Doxygen、JSDoc 等)
        • 类型系统特性(泛型、模板)
        • 属性/getter/setter 模式
      • 代理必须使用 ripgrep 而不是 grep!
      • 另存为 ${TARGET}-conventions.md
      • 停止并请用户审查

    b. 完整读取 porting-notes.md

    • 如果缺失则创建,内容为:
    # 移植笔记
    

2. 移植类型(每个重复执行)

  1. 查找下一个待处理类型:

    # 获取下一个待处理类型信息及候选文件
    jq -r '.portingOrder[] | {file: .javaSourcePath, types: .types[] | select(.portingState == "pending")} | "\(.file)|\(.types.name)|\(.types.kind)|\(.types.startLine)|\(.types.endLine)|\(.types.candidateFiles | join(","))"' porting-plan.json | head -1
    
  2. 通过 vs-claude 在 VS Code 中打开文件(供用户审阅):

    • 使用 vs-claude 打开 Java 文件以及 Java 文件的 git diff(从 prevBranch 到 currentBranch)
    • 如果存在 candidateFiles:使用 vs-claude 打开所有候选文件
  3. 与用户确认:

    • 提问:“移植此类型?(y/n)”
    • 停止并等待确认。
  4. 读取源文件并分析变更:

    • 读取 Java 类型 diff 以查看当前代码和变更:

      ./read-java-type-diff.js <type-name>
      
      • 如果 diff 只显示未变更行(没有 +- 前缀):
        • 告知用户:“未检测到 <type-name> 的变更。标记为完成?(y/n)”
        • 如果是,跳转到步骤 6 更新状态
        • 如果否,继续分析目标文件(可能需要在目标处进行变更)
    • 如果类型 extends/implements 了其他类型,读取父类型:

      • 检查类型声明中的 extends/implements
      • 对每个父类型使用 ./read-java-type.js &lt;parent-type>
      • 递归进行,直到获得完整的继承链
    • 如果存在目标候选文件,读取它们:

      • 检查 porting-plan.json 中的 candidateFiles 数组
      • 完整读取每个候选文件以了解当前目标实现
      • 并行读取候选文件
  5. 移植类型:

    • 关键:目标是与 Java(当前分支)实现 100% 功能对等

    • 分析方法:

      • 首先,了解当前 Java 实现的内容(所有字段、方法、内部类)
      • 其次,了解当前目标实现的内容
      • 第三,识别差异:
        1. Java 中有而目标缺失 → 添加
        2. 目标中有而 Java 没有 → 移除(除非是习惯性差异)
        3. 两者都有但不同 → 更新以匹配 Java
        4. 两者相同 → 保持不变
    • 每个差异的决策框架:

      • 这是否是习惯性差异?
        • 如果是 → 保留目标的习惯性做法,但确保功能一致
      • 这是否是 Java 已移除的旧功能?
        • 如果是 → 从目标中移除
      • 这是否是 Java 新增的新功能?
        • 如果是 → 添加到目标
      • 这是否是行为上的差异?
        • 如果是 → 更新目标以完全匹配 Java 行为
    • 实施步骤:

      • 如果目标文件不存在,按照约定创建
      • 系统地进行更改:
        1. 先移除过时代码
        2. 更新现有代码(签名,然后实现)
        3. 最后添加新代码
      • 对一个文件的所有更改使用 MultiEdit
      • 对于 C++:在重大更改后运行 ./compile-cpp.js
      • 更新文档(doxygen/jsdoc)以匹配 Java
    • 验证检查清单:

      • 所有 Java 的 public/protected 成员在目标中存在
      • 目标中没有多余的 public/protected 成员(除非是习惯性差异)
      • 所有方法行为完全匹配,尤其是数学密集型代码
      • 所有常量和枚举匹配
      • 在非托管语言(如 C++)中内存管理正确
      • 目标运行时代码遵循目标语言约定
  6. 获得用户确认:

    • 打开你修改过的文件的 diff,比较 HEAD 与工作区。
    • 向用户总结你移植了什么
    • 提问:“标记为完成?(y/n)”
    • 如果是,更新状态:
    jq --arg file "path/to/file.java" --arg type "TypeName" \
       '(.portingOrder[] | select(.javaSourcePath == $file) | .types[] | select(.name == $type) | .portingState) = "done"' \
       porting-plan.json > tmp.json && mv tmp.json porting-plan.json
    
  7. 更新 porting-notes.md:

    • 添加任何新发现的模式或特殊情况。
  8. 停止并确认:

    • 显示移植的内容。提问:“继续下一个类型?(y/n)”
    • 只有在确认后才能继续。
  • 原文链接: github.com/badlogic/spin...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论