背景

swf_reader 最初是一个 C# 库(作者 Sergey Savchuk,MIT 协议),被我移植到了 C++ 并嵌入 Godot 作为模块使用。

C# 版大量使用 try/catch/throw 来做错误处理——解析 SWF 文件时,遇到格式错误直接抛异常。这在 C# 里是完全正常的做法,但移植到 C++ 后在 Godot 的编译环境下遇到了一个问题:

Godot 编译时默认启用 -fno-exceptions,禁止使用 C++ 异常。

这意味着所有 throwtrycatch 都不能用,编译直接报错。


问题规模

原代码中异常的使用情况统计:

用途出现次数典型场景
解析错误47SWF 标签格式异常、数据截断
类型转换失败12形状数据类型不匹配
资源不存在8引用未定义的 SWF 资源
断言类15内部状态不一致

总共 82 处异常相关的代码需要处理。


方案选择

方案 A:错误码返回

每个可能失败的函数改为返回 ErrorCode 枚举值,数据通过出参传递。

方案 B:Optional / Result 类型

使用 std::optional 或自制的 Result<T, ErrorCode> 包装返回值。

方案 C:全局错误状态

类似 OpenGL 的 glGetError(),函数内部记录错误,调用方主动查询。


我最终选择了方案 B,主要是因为:

  1. 错误信息不会丢失——可以附带错误描述
  2. 调用方必须显式处理错误([[nodiscard]] 强制)
  3. 代码表达力比错误码更强

Result 类型的实现

// swf_error.h
namespace swf {

enum class ErrorCode {
    OK = 0,
    PARSE_ERROR,
    UNSUPPORTED_FEATURE,
    RESOURCE_NOT_FOUND,
    INVALID_DATA,
    OUT_OF_MEMORY
};

template <typename T>
class Result {
public:
    Result(T value) : m_value(std::move(value)), m_code(ErrorCode::OK) {}
    Result(ErrorCode code) : m_code(code) {}

    bool is_ok() const { return m_code == ErrorCode::OK; }
    bool is_error() const { return m_code != ErrorCode::OK; }

    T& value() {
        // 用于已知不会出错的地方
        return m_value;
    }

    ErrorCode error() const { return m_code; }

private:
    std::optional<T> m_value;
    ErrorCode m_code;
};

} // namespace swf

注意:这里用 std::optional 的前提是你的编译环境支持 C++17。Godot 4.x 默认是 C++17,所以没问题。


重构示例

原 C# 代码

public byte[] ReadBytes(int count)
{
    if (_position + count > _data.Length)
        throw new SwfParseException("Unexpected end of data");
    
    var result = new byte[count];
    Array.Copy(_data, _position, result, 0, count);
    _position += count;
    return result;
}

重构后的 C++ 代码

Result<Vector<uint8_t>> SwfReader::read_bytes(int count) {
    if (_position + count > _data.size()) {
        return ErrorCode::PARSE_ERROR;
    }

    Vector<uint8_t> result;
    result.resize(count);
    memcpy(result.ptrw(), _data.data() + _position, count);
    _position += count;
    return result;
}

// 调用方
auto result = reader.read_bytes(4);
if (result.is_error()) {
    // 错误处理
    return ErrorCode::PARSE_ERROR;
}
uint32_t tag_type = read_uint32_le(result.value());

与 Godot 的错误处理机制结合

Godot 自身也提供了几个错误处理宏:

ERR_FAIL_COND_V(condition, retval);   // 条件失败时返回
ERR_FAIL_NULL(ptr);                    // 指针为空时返回
CRASH_COND(condition);                 // 条件失败时直接崩溃

对于模块内部,我保留了这些宏用于不可恢复的错误(比如内部状态不一致),而对于可恢复的解析错误(比如用户传了一个损坏的 SWF 文件),使用 Result 类型更合适。

混合使用的原则是:


性能影响

实际测试下来,Result 类型的性能开销几乎可以忽略:

对比原始异常方案,在正常解析路径上,两种方案的汇编产物几乎没有区别。只有在错误路径上,Result 比异常略慢(因为需要显式检查),但这本就是错误路径,不关键。


总结

移除 C++ 异常本身并不难,但它迫使你更显式地处理错误。表面上看是多写了几行 if (result.is_error()),但带来的好处是:

  1. 调用方不能忽视错误——[[nodiscard]] 让编译器帮你检查
  2. 控制流更清晰——没有隐藏的栈展开
  3. Godot 兼容——不再依赖 -fexceptions
  4. 调试更简单——错误路径肉眼可见,不需要在 catch 块里打断点

这大概是从"写起来爽"到"用起来稳"的转变中,必须付出的代价。


下一篇预告:SWF 解析器中的 Box 与 incomplete type 问题——unique_ptr 的隐藏陷阱。