UniInk 是一个专为 AOT(Ahead-of-Time)编译场景设计的高性能 C# 表达式解释器库,特别适用于 Unity IL2CPP 环境。
- 项目名称: UniInk-CSharpInterpreter4AOT
- 作者: Arc (Arc-huangjingtong)
- 版本: 1.1.0
- 许可证: MIT License
- 目标框架: .NET Framework 4.8+
- 语言版本: C# 9.0+
- 代码行数: 约 13,000 行(47个 C# 文件)
Arc.UniInk/
├── Arc.UniInk/ # 核心库项目
│ ├── UniInk.cs # 主要解释器实现 (2,317 行)
│ ├── UniInk_Extensions.cs # 扩展功能 (322 行)
│ └── UniInk_AssemblyInfo.cs # 程序集信息
├── Arc.UniInk.Benchmark/ # 性能基准测试
│ ├── Benchmark_UniInkSpeed/ # UniInk 性能测试
│ ├── Refer_Sprache/ # Sprache 库参考实现
│ ├── Refer_ExpressionEvaluator/ # ExpressionEvaluator 参考
│ └── Refer_ParsecSharp/ # ParsecSharp 参考
└── Arc.UniInk.BestPractices/ # 最佳实践示例
UniInk 采用单脚本设计作为核心架构理念,将整个解释器集成在单个文件中。这种设计具有以下显著优势:
- 极简部署: 仅需复制一个文件即可集成到任何项目
- 零依赖: 不依赖外部库,避免版本冲突和依赖地狱
- 便于移植: 开发人员可以轻松地移植和修改代码
- 轻量化: 保持最小的代码体积和运行时开销
- 自包含: 所有功能都在一个文件中,便于理解和维护
这种设计特别适合:
- 需要快速集成表达式解析功能的项目
- 对部署简单性有严格要求的环境
- 需要定制化修改的场景
- Unity 等游戏引擎的嵌入式脚本需求
- 对象池机制: 使用
Queue<InkValue>实现对象复用 - 值类型优化: 直接存储基本类型值,避免装箱
- 内存管理: 显式的
Get()和Release()方法管理内存
// 核心 API 使用示例
var ink = new UniInk();
var result = ink.Evaluate("3 + 5 * 2") as InkValue;
Console.WriteLine(result.Value_int); // 输出: 13
InkValue.Release(result); // 显式释放,实现零 GC- 基本类型: int, float, double, bool, char, string
- 对象类型: 任意 C# 对象
- 字符串处理: 使用
List<char>实现零 GC 字符串拼接
- 算术运算:
+,-,*,/,% - 逻辑运算:
&&,||,! - 比较运算:
==,!=,>,<,>=,<= - 括号表达式: 支持嵌套括号
- 变量赋值:
var a = 123; var b = a + 1; - 函数调用: 支持注册和调用 C# 函数
- Lambda 表达式: 支持自定义谓词函数
- 属性获取器: 动态属性访问
- 控制流: If-else 语句支持
根据项目中的基准测试数据:
| 方法 | 平均时间 | 内存分配 | 相对性能 |
|---|---|---|---|
| UniInkSpeed | 296.06 μs | 68 B | 1.00x (基准) |
| UniInkSpeed (编译) | 164.65 μs | 658 B | 0.56x |
| ExpressionEvaluator | 2,621.71 μs | 2,068,334 B | 8.87x |
| Sprache | 2,678.89 μs | 2,817,145 B | 9.05x |
| ParsecSharp | 1,063.70 μs | 851,830 B | 3.59x |
结论: UniInk 在性能和内存使用方面显著优于其他解析库。
public InkSyntaxList CompileLexerAndFill(string expression, int startIndex, int endIndex)
{
var keys = InkSyntaxList.Get();
for (var i = startIndex; i <= endIndex; i++)
{
if (char.IsWhiteSpace(expression[i])) continue;
var any = false;
foreach (var parsingMethod in ParsingMethods)
{
if (parsingMethod(expression, keys, ref i))
{
any = true;
break;
}
}
// 错误处理...
}
return keys;
}EvaluateOperators- 运算符解析EvaluateFunction- 函数调用解析EvaluateNumber- 数字解析EvaluateChar- 字符解析EvaluateString- 字符串解析EvaluateBool- 布尔值解析EvaluateVariable- 变量解析
public partial class InkValue
{
public static readonly Queue<InkValue> pool = new(UniInk.INK_VALUE_POOL_CAPACITY);
public static InkValue Get()
{
GetTime++;
return pool.Count > 0 ? pool.Dequeue() : new();
}
public static void Release(InkValue value)
{
if (value == null || value.dontRelease) return;
// 重置对象状态
pool.Enqueue(value);
ReleaseTime++;
}
}- 性能优化: 零 GC 设计和对象池机制
- 架构清晰: 分离词法分析、语法分析和执行阶段
- 可扩展性: 易于添加新的运算符和函数
- 文档完善: 丰富的代码注释和使用示例
- 基准测试: 完整的性能对比测试
- 单脚本设计: 整个解释器集成在单个文件中,便于移植和部署
- 错误处理: 异常信息可以更加详细
- 类型安全: 一些类型转换可以更加安全
- 平台兼容: 目前仅支持 .NET Framework 4.8
- 优点: 功能强大的解析器组合子库
- 缺点: 性能较低,内存分配较多
- 适用场景: 复杂语法解析
- 优点: 功能全面
- 缺点: 性能最低,内存消耗最大
- 适用场景: 功能完整性要求高的场景
- 优点: 函数式解析器组合子
- 缺点: 中等性能和内存使用
- 适用场景: 函数式编程偏好
- Unity 游戏开发: IL2CPP 环境下的表达式计算
- 高性能应用: 需要频繁表达式计算的场景
- 嵌入式脚本: 轻量级脚本执行引擎
- 配置驱动: 动态配置表达式求值
// 1. 创建解释器实例
var ink = new UniInk();
// 2. 注册函数和变量
ink.RegisterFunction("SUM", new InkFunction(SumFunction));
ink.RegisterVariable("age", InkValue.GetIntValue(25));
// 3. 执行表达式
var result = ink.Evaluate("SUM(1, 2, age)") as InkValue;
// 4. 获取结果并释放内存
var value = result.Value_int;
InkValue.Release(result);- 增强测试: 添加更多边界条件测试
- 文档改进: 提供更多使用示例和 API 文档
- 错误诊断: 提供更详细的错误信息和调试支持
- 跨平台支持: 迁移到 .NET Standard/.NET 5+
- NuGet 包: 发布官方 NuGet 包
- 语法扩展: 支持更多 C# 语法特性
- 可视化工具: 提供表达式调试和分析工具
UniInk 是一个设计优秀的高性能 C# 表达式解释器,特别适合 AOT 编译环境。其零 GC 设计和对象池机制使其在性能方面显著优于同类库。单脚本架构的设计理念使其具有极佳的便携性和部署简单性,这是该项目的核心优势之一。
- 极致性能: 零 GC 设计,性能优于同类库 3-9 倍
- 单脚本设计: 便于移植、部署和定制化修改
- 轻量化: 最小依赖,自包含设计
- AOT 兼容: 完美支持 Unity IL2CPP 等 AOT 环境
对于需要在性能敏感环境中进行表达式计算的项目,特别是需要简单部署和高度定制化的场景,UniInk 是一个优秀的选择。