cvmruntime:基于 C 语言的通用虚拟机运行时项目

可嵌入 C/C++ 项目作为脚本引擎或独立作为命令行工具,解释执行类似 C 的脚本语言,支持变量、函数、闭包、控制流、字符串、数组、数学运算及 FFI,采用纯 C 编写,零外部依赖,具备模块化设计与安全沙箱机制。【此简介由AI生成】

分支1Tags1
文件最后提交记录最后更新时间
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(C double ↔ CVM number
    • i = int(C int32 ↔ CVM numbernumber(int) 互转)
    • q = int64(C int64_t ↔ CVM int
    • p = ptr(C void* ↔ CVM ptr新增 VAL_PTR 载体
    • s = str(C char* ↔ CVM string;入参直接传 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 捕获,异常值绑定到 efinally必定执行(即使 catch 中再抛)。
  • 边界(设计内,务必知晓)
    • 只有显式 throwcatch;运行时错误(除零、未定义变量、下标越界)原生崩溃(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.cvmpackages/<名>.cvmpackages/<名>/<名>.cvm;也支持直接传文件路径(以 .cvm 结尾,原样使用)。
  • 每个模块在隔离环境中执行,顶层 let / func 会被收集为导出成员。
  • 已加载的模块按路径缓存,重复 require 不重复执行。
  • 成员访问 m.x 与点调用 m.x(args) 是语法糖。

解析规则详解

  1. 官方/嵌套包:require("a/b") 会按 packages/a/b/index.cvmpackages/a/b.cvmpackages/a/b/b.cvm 顺序查找(a/b 即"嵌套包",类似 npm 子路径)。 例:require("strings") 解析到 packages/strings/index.cvm(见 packages/strings/)。
  2. 相对路径:以 ./../ 开头时,相对当前正在执行的脚本所在目录解析, 并自动规整 . / .. 段(samples/./cyc_bsamples/cyc_b)。 例:samples/demo_rel.cvmrequire("./demo_sibling")samples/demo_sibling.cvm
  3. 循环依赖检测:若加载链成环(如 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 + 数组 ikeyint64_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/运算符重载。

设计取舍:代码从单文件拆成模块化多文件以提升可维护性。模块系统刻意做成"平台层",使内核保持通用、不绑定任何特定应用。

项目介绍

可嵌入 C/C++ 项目作为脚本引擎或独立作为命令行工具,解释执行类似 C 的脚本语言,支持变量、函数、闭包、控制流、字符串、数组、数学运算及 FFI,采用纯 C 编写,零外部依赖,具备模块化设计与安全沙箱机制。【此简介由AI生成】

定制我的领域