可嵌入 C/C++ 项目作为脚本引擎或独立作为命令行工具,解释执行类似 C 的脚本语言,支持变量、函数、闭包、控制流、字符串、数组、数学运算及 FFI,采用纯 C 编写,零外部依赖,具备模块化设计与安全沙箱机制。【此简介由AI生成】
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 |
cvmruntime
一门 C 风格脚本语言的纯 C 解释器运行时。
cvmruntime是一个通用的 C 语言虚拟机/运行时,用纯 C 写成,零外部依赖。 解释执行一门类似 C 的脚本语言——变量、函数、闭包、控制流、字符串、数组、数学、FFI。 可嵌入任何 C/C++ 项目作为脚本引擎,或独立作为命令行工具使用。
一、架构
script.cvm ──► 词法(Lexer) ──► 语法(Parser) ──► AST ──► 树遍历解释器(Interpreter)
│
┌─────────────────────────────┼──────────────────┐
▼ ▼ ▼
环境帧 EnvFrame 宿主 API 注册表 FFI 管理器
(哈希表 + 父作用域链) (print/sqrt/...) (加载 .dll/.so)
| 模块 | 文件 | 职责 |
|---|---|---|
| 内存 | cvm_arena.c |
bump 式 arena 分配器,全程复用,避免零散泄漏 |
| 映射/环境 | cvm_env.c |
通用字符串→指针 Map + 词法作用域 Env(沿 parent 链查找) |
| 词法 | cvm_lexer.c |
注释 / 数字 / 字符串转义 / 标识符关键字 / 运算符 |
| 语法 | cvm_parser.c |
递归下降解析 + AST 构造器(优先级: 或→与→相等→比较→加减→乘除→一元→主) |
| 解释器 | cvm_interp.c |
求值器 eval_expr + 语句执行器 exec_stmt + 函数调用分派 |
| 数组 | cvm_arr.c |
PHP 风格有序关联数组(list+map 合一,哈希表 + 插入顺序) |
| 宿主 API | cvm_host.c |
内置函数(print / sqrt / len / type / require / 数组 ...) |
| FFI | cvm_ffi.c |
跨平台加载 .dll/.so,符号缓存,带后缀自动补全 |
| 模块加载 | cvm_mod.c |
Node 风格包加载器(require 解析 packages/,隔离环境,导出表缓存) |
| 入口 | main.c |
文件模式 + REPL(支持多行续行) |
值模型:number(double) / int(int64 真整数) / string / bool / nil / module(指向导出 Map) / func(已注册函数名) / array(PHP 风格有序关联数组) / ptr(FFI 原生指针,见 FFI 段)。
函数分派优先级:宿主内置 → 用户函数 → FFI 外部函数。
类比 Node/V8:
cvmruntime的解释器内核就是"引擎"(类 V8);require+packages/这套模块系统就是"平台层"(类 Node 的 npm 生态)。本运行时刻意做成通用内核, 不绑定任何特定应用。
设计参考 / 对标学习:见
docs/language-study.md——PHP/Lua/V8/Python 等语言内部实现(数组/表/闭包/字节码)的可复用研究笔记,及其对 cvmruntime 的取舍启示。
二、构建
需要 MinGW-w64 GCC(本机位于 C:\msys64\mingw64\bin\gcc.exe),标准:gcc -Wall -Werror -std=c11。
# Windows
build.bat
# 或通用
make
# 或手动
gcc -Wall -Werror -std=c11 -O2 -o cvmruntime main.c cvm_arena.c cvm_env.c \
cvm_lexer.c cvm_parser.c cvm_interp.c cvm_arr.c cvm_host.c cvm_ffi.c cvm_mod.c -lm
产物:cvmruntime.exe(或 cvmruntime)。
三、语言速查
类型
| 字面量 | 类型 |
|---|---|
42 / 100000000000000 |
int(int64 真整数,lexer 不经 double 往返,精确打印,无后缀) |
3.14 / 1e6 |
number(double 浮点) |
"hello" |
string(支持 \" \\ \n \t \r \0) |
true / false |
bool |
nil |
nil(空值) |
[1, 2, 3] / ["k": "v"] |
array(有序关联数组) |
func(x){...} |
closure(闭包/一等函数) |
变量与作用域
let x = 10; // 声明(进入当前作用域)
x = x + 1; // 赋值(沿作用域链向上找, 找不到则建为全局)
let 在函数/块内声明局部变量;无 let 的赋值会就近修改,找不到则新建全局。
函数
func add(a, b) {
return a + b; // 可省略分号? 不行, 语句以 ; 结尾 (C 风格)
}
println(add(3, 4)); // 7
支持递归与前向引用(顶层函数预注册)。
控制流
if (n < 2) return n; // 单语句体可不带花括号
if (x > 0) { println("正"); }
else if (x < 0) { println("负"); }
else { println("零"); }
while (i <= 10) { sum = sum + i; i = i + 1; }
for (let k = 1; k <= 10; k = k + 1) { s = s + k; }
for (let i = 0; i < 10; i = i + 1) { if (i == 3) break; } // break 跳出循环
for (let i = 0; i < 10; i = i + 1) { if (i == 3) continue; } // continue 跳到下次迭代
for (k, v in arr) { println(k + ": " + v); } // for-in: 遍历数组(键, 值)
for (x in arr) { println(x); } // 单变量形式(仅值)
表达式
算术 + - * / %、比较 == != < > <= >=、逻辑 and or not(短路)、
字符串 + 拼接、括号分组、函数调用。
宿主内置
print println sqrt pow abs floor ceil round rand
time clock_ms len type str num input import ffi_enable ffi_decl
require(模块加载) str_upper str_lower str_substr str_contains
str_index str_rindex str_replace str_split str_join str_trim str_ltrim str_rtrim str_starts str_ends str_reverse str_repeat str_count str_len str_char_at str_ord
file_read file_write file_exists
push arr_get arr_set arr_has arr_del arr_keys arr_values arr_merge
闭包 (Lambda + 词法捕获)
一等函数 + 词法闭包(Lua upvalue 风格)。利用 arena Value* 槽运行期不释放的特性,省去 Lua 式的栈槽迁移。通过 func(params) { body } 字面量创建匿名函数,自动捕获外层局部变量(按共享引用,非值复制)。
let add = func(x, y) { return x + y; }; // lambda 作为值
println(add(3, 4)); // 7
func make_adder(x) { return func(y) { return x + y; }; }
let add5 = make_adder(5);
println(add5(3)); // 8 (捕获外层 x)
func make_counter() {
let n = 0;
return func() { n = n + 1; return n; }; // 可变状态共享
}
let c = make_counter();
println(c()); // 1
println(c()); // 2
func fact(n) { if (n <= 1) return 1; return n * fact(n - 1); }
println(fact(5)); // 120
数组(PHP 风格有序关联数组)
对标 PHP 的 array——一个类型同时充当 list(整数键)与 map(字符串键),且保留插入顺序。
是 cvmruntime 目前唯一的集合类型,也是写真实程序的基础。
// 列表: 整数键自动从 0 递增
let nums = [10, 20, 30];
println(nums[0]); // 10
nums[1] = 99; // 下标原地写入
push(nums, 40, 50); // 追加
// map: 用 "键": 值 形式声明字符串键
let user = ["name": "Alice", "age": 30];
println(user["name"]); // Alice
// 二者合一: list 与 map 可以在同一数组里共存
let mix = [1, 2, "k": "v", 3];
// 下标可以是任意数字/字符串/布尔表达式, 且支持链式 a[i][j]
let nested = [["a", "b"], "deep": [1, [2, 3]]];
println(nested["deep"][1][0]); // 2
// 引用语义: 多个变量可指向同一数组并看到彼此改动
let ref = nums; push(ref, 999);
// 结构相等(含嵌套)
[1, [2,3]] == [1, [2,3]]; // true
数组内置
| 函数 | 说明 |
|---|---|
len(arr) |
元素个数(字符串同样适用) |
push(arr, v, ...) |
追加元素,返回数组本身 |
arr_get(arr, key) |
取值,不存在返回 nil |
arr_set(arr, key, v) |
设值,返回数组本身 |
arr_has(arr, key) |
键是否存在 → bool |
arr_del(arr, key) |
删除键,成功 1 / 不存在 0 |
arr_keys(arr) |
返回新数组(键的列表: 数字键为 number,字符串键为 string) |
arr_values(arr) |
返回新数组(值的列表) |
arr_merge(a, b) |
返回新数组(a、b 合并,b 覆盖同名键) |
设计取舍:数组按引用语义共享(同 Lua table / Python list),而非 PHP 的写时复制; 数据分配在 arena,随运行时销毁,不引入 GC。下标键可用数字、字符串或布尔;
a[0]与a["0"]被视为不同键(不做 PHP 式的隐式类型合并),行为更可预测。
FFI(调用外部动态库 · 外部 allowlist + 类型化签名 · v1.1 真沙箱已落地)
⚠️ 真沙箱模型:FFI 由运维通过
cvmruntime.ffi配置文件预先授权「哪些库 + 哪些符号」可用;脚本不可自行ffi_enable()绕过白名单——调用ffi_enable()直接报「脚本不可自启」。ffi_decl也只能为白名单内的符号声明签名。未在白名单的库/符号一律被拦截,脚本无法自行提权调用任意原生符号(RCE 已杜绝)。白名单文件
cvmruntime.ffi格式(#注释,每行<库路径> : <符号1,符号2,...>):msvcrt : strlen, _atoi64 samples/mylib.dll : cvm_add, cvm_sub
开启后,每个外部符号必须先声明类型化签名才能调用:
// 运维已在 cvmruntime.ffi 中授权 samples/mylib.dll:cvm_add
ffi_decl("cvm_add", "dd)d"); // 声明签名 (仅白名单内符号)
import("samples/mylib.dll"); // 加载库 (须在白名单)
println(cvm_add(2, 3)); // 5 (按 double 正确封送)
ffi_decl("str_len", "s)i"); // (char*) -> int
ffi_decl("get_name", "v)s"); // () -> char* (void 参数, 返回字符串)
- 签名串格式
"<参数类型>)<返回类型>",右括号分隔参数区与返回区;类型字母:d= double(Cdouble↔ CVMnumber)i= int(Cint32↔ CVMnumber,number→(int)互转)q= int64(Cint64_t↔ CVMint)p= ptr(Cvoid*↔ CVMptr,新增VAL_PTR载体)s= str(Cchar*↔ CVMstring;入参直接传 arena 稳定指针,返回arena_strdup拷回)v= void(仅用于「返回」位,表示无返回值)
- 为什么:旧实现把所有参数强转
double、字符串按(double)(intptr_t)str塞入,64 位下指针经 double 往返必丢高位(UB)。v1 改为按签名逐参封送,p走真实void*槽,彻底消除该 UB。 - 编译库:
gcc samples/mylib.c -shared -o samples/mylib.dll
异常 (try / throw · 仅显式 throw 可 catch)
try {
throw "出错了";
} catch (e) {
println("捕获: " + e); // 捕获: 出错了
} finally {
println("无论是否异常都执行");
}
try { } catch (e) { } finally { }:进入 try 块;throw <值>抛出异常,被最近的catch捕获,异常值绑定到e;finally块必定执行(即使 catch 中再抛)。- 边界(设计内,务必知晓):
- 只有显式
throw可catch;运行时错误(除零、未定义变量、下标越界)与原生崩溃(SIGSEGV / abort)都不可捕获,会直接终止脚本。 - 异常可跨函数帧传播(被调函数内 throw,由更外层 try 捕获);顶层仍未捕获时,stderr 单次打印错误(已修重复打印回归)。
- 所以 FFI 调用期内的原生故障不在 CVM 异常域内——这与
docs/proposal-ffi.md§2.5 的预留一致。
- 只有显式
四、模块与包系统(类 Node)
把 cvmruntime 当成"内核 + 平台"两层:内核是解释器(类 V8),平台层就是这套
require 模块系统(类 Node 的 npm 生态)。目标是让脚本能像 Node 一样 require
一个"包"拿到命名空间,然后点出成员。
用法
let math = require("math"); // 返回命名空间(导出表 Map)
let str = require("str");
let fs = require("fs");
println(math.fact(5)); // 点出成员: 120
println(str.upper("hi")); // HELLO
println(fs.read("samples/hello.cvm"));
require("包名")解析顺序:packages/<名>/index.cvm→packages/<名>.cvm→packages/<名>/<名>.cvm;也支持直接传文件路径(以.cvm结尾,原样使用)。- 每个模块在隔离环境中执行,顶层
let/func会被收集为导出成员。 - 已加载的模块按路径缓存,重复
require不重复执行。 - 成员访问
m.x与点调用m.x(args)是语法糖。
解析规则详解
- 官方/嵌套包:
require("a/b")会按packages/a/b/index.cvm→packages/a/b.cvm→packages/a/b/b.cvm顺序查找(a/b即"嵌套包",类似 npm 子路径)。 例:require("strings")解析到packages/strings/index.cvm(见packages/strings/)。 - 相对路径:以
./或../开头时,相对当前正在执行的脚本所在目录解析, 并自动规整./..段(samples/./cyc_b→samples/cyc_b)。 例:samples/demo_rel.cvm里require("./demo_sibling")→samples/demo_sibling.cvm。 - 循环依赖检测:若加载链成环(如
A→B→A),直接报require: 检测到循环依赖,不会无限递归。见samples/cyc_a.cvm/cyc_b.cvm。
官方包(packages/)
| 包 | 成员 | 说明 |
|---|---|---|
math |
add sub mul div fact gcd is_prime |
数学函数 |
str |
upper lower substr contains |
字符串处理(底层为 str_* 内置) |
fs |
read write exists |
文件读写(底层为 file_* 内置) |
写自己的包
在 packages/ 下新建目录或 .cvm 文件,顶层声明即导出:
// packages/mypkg.cvm
func greet(name) { return "hi, " + name; }
let version = "1.0";
使用:let p = require("mypkg"); println(p.greet("cvm"));
五、运行
cvmruntime.exe samples/hello.cvm # 文件模式
cvmruntime.exe samples/modules.cvm # 模块系统演示(官方包)
cvmruntime.exe samples/demo_rel.cvm # 嵌套包 + 相对路径 + 普通包
cvmruntime.exe samples/cyc_a.cvm # 循环依赖检测演示(应报循环依赖)
cvmruntime.exe samples/arr_demo.cvm # 数组演示(list/map/嵌套/引用/相等)
cvmruntime.exe # REPL 模式 (输入 exit 退出)
六、当前状态与路线图
已实现
- 词法 / 语法 / AST / 树遍历解释器
- 值模型:
number(double) /int(int64 真整数,字面量不经 double 往返) /ptr(FFI 原生指针) /string/bool/nil/module/func/array;词法作用域、用户函数、递归 - if / while / for(带/不带花括号体) / break / continue / for-in 迭代器
(k,v) in arr - 宿主内置标准库(含 20 个字符串内置 +
ffi_enable/ffi_decl);FFI v1 弱沙箱(默认禁用 + 类型化签名封送 +VAL_PTR) - 异常 try / throw / finally(仅显式 throw 可 catch;运行时错误/SIGSEGV 不可捕获;finally 必跑)
- 文件执行 + REPL(多行续行)、错误报告(行号)
- 模块/包系统:Node 风格
require+packages/目录 + 导出表缓存 + 3 个官方包(math/str/fs)- 支持嵌套包(
require("a/b")→packages/a/b/index.cvm等) - 支持相对路径
require("./sib")/require("../x")(相对当前脚本目录,段级规整./..) - 循环依赖检测(加载链成环时报错,不无限递归)
- 支持嵌套包(
- PHP 风格有序关联数组(
array类型):list 与 map 合一、插入顺序、引用语义、下标读写a[i]/a["k"]/a[i]=v、嵌套与链式下标、结构相等;内置push/arr_get/arr_set/arr_has/ arr_del/arr_keys/arr_values/arr_merge(见samples/arr_demo.cvm)。这是对标 PHP 的第一刀。 - 字符串家族(共 20 个
str_*内置):str_upper/str_lower/str_substr/str_contains/str_index/str_rindex/str_replace/str_split/str_join/str_trim/str_ltrim/str_rtrim/str_starts/str_ends/str_reverse/str_repeat/str_count/str_len(码点计数)/str_char_at/str_ord;s[i]码点感知下标;s[i]=v替换码点(兼容 char/ord,创建新串)。边界:len仍字节数、str_substr仍字节切片、+仍拼接、==仍字节比较;upper/lower做 Unicode 大小写(Latin-1/Greek/Cyrillic 三块);str_split末尾分隔符产生尾部空字段(对齐 Python)。 - 闭包(一等函数 + 词法捕获, Lua upvalue 风格):
func(x){...}lambda 字面量, 按共享引用捕获外层局部, 支持递归闭包与多闭包共享可变状态。利用了 arena Value* 槽运行期不释放的特性, 省掉了 Lua 式的栈槽迁移(6/6 回归绿灯, 详见tests/t_closure.cvm) - 递归深度守卫:
CVM_MAX_CALL_DEPTH=2048,超限经interp_err优雅报错(详见tests/t_recursion_limit.cvm) - int64 真整数:
VAL_INT+ 真 int64 字面量 + 整除向零截断 + int/double 混合提升为 double + 数组ikey改int64_t(消 LLP64 32 位截断);相等语义保持 value-equal(int==num按值比,同 JS/Python)。详见tests/t_int.cvm。 - FFI v1.1 真沙箱:外部 operator allowlist(
cvmruntime.ffi配置文件, 格式库 : 符号1,符号2) + 禁止脚本 self-authorize(ffi_enable()永久报错) +b_import/ffi_decl/ffi_resolve_allowed三处闸门联动,杜绝脚本自启调用任意原生符号(RCE)。详见tests/t_ffi_*.cvm。
已知缺口 / 后续项(见 docs/roadmap.md §6)
- 【架构】arena 进程级生命周期:运行期不回收,长运行/REPL/服务场景内存只增不减(无 GC/分代);已声明边界。
- 【表达力】 未做:
packed-array快路径、字节码 VM 执行器接线(编译器已就位)、metatable/运算符重载。
设计取舍:代码从单文件拆成模块化多文件以提升可维护性。模块系统刻意做成"平台层",使内核保持通用、不绑定任何特定应用。