- 编程语言
- 语言运行时
- 编译器
【免费下载链接】wren
The Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.
Wren 是一款面向嵌入式场景设计的解释型脚本语言,官方将其概括为 "a small, fast, class-based concurrent scripting language"。本文以仓库根目录的 README.md 为主线,逐条拆解 Wren 的五大设计支柱(小、快、基于类、并发、可嵌入),并结合本仓库中的 VM 源码、C API 头文件、官方文档与测试用例给出源码级证据,最后给出完整的构建与嵌入 C 程序的可运行方案。读完本文,你将掌握 Wren 的语法风格、执行模型,以及如何把 Wren VM 作为库嵌入到自己的 C/C++ 程序中。
一句话认识 Wren:Smalltalk 的思想、Lua 的体积、Erlang 的并发
README 开篇用一句话给出了 Wren 的定位:
Think Smalltalk in a Lua-sized package with a dash of Erlang and wrapped up in a familiar, modern syntax.
也就是说,Wren 借鉴了 Smalltalk 的纯面向对象思想、Lua 级别的极简体积,并从 Erlang 的并发理念中抽取了轻量协程模型,最后用一套现代而熟悉的 C 系语法包装起来。README 给出的示例代码同时展示了类、Fiber 协程和字符串插值三种核心能力:
System.print("Hello, world!") class Wren { flyTo(city) { System.print("Flying to %(city)") } } var adjectives = Fiber.new { ["small", "clean", "fast"].each {|word| Fiber.yield(word) } } while (!adjectives.isDone) System.print(adjectives.call())这段代码可以拆解为三个语言特性:System.print与%(city)字符串插值属于标准输出与语法层面;class Wren声明类并定义方法体现其基于类的对象模型;Fiber.new { ... }创建协程、Fiber.yield(word)挂起当前协程、.call()唤醒协程,则是 Wren 并发模型的核心用法。这一小段示例几乎浓缩了整门语言的精华。
五大设计支柱:从 README 到源码的逐条验证
小:整个 VM 实现不足 4000 个分号
README 宣称 "The VM implementation is under 4,000 semicolons",并强调它 "small, but not dense"——即代码量小但可读性高、注释详尽。这一点可以直接在仓库中验证:VM 的全部实现集中在 src/vm 目录下,包含编译器 wren_compiler.c(约 4140 行)、虚拟机主循环 wren_vm.c(约 1994 行)、值表示 wren_value.c 等文件。对src/vm下所有.c/.h文件的粗略统计,分号总数约 3600 余个,与 README "under 4,000 semicolons" 的说法一致。
体积小的另一面是依赖极少:src/README.md 说明源码仅由include(公共头文件目录)、vm(VM 本体)、optional(可选模块)三部分组成,官方文档也明确 Wren "has no dependencies beyond the C standard library"。代码同时兼容 C99 与 C++98 及之后的任何标准,这为嵌入到各种宿主项目提供了极大的便利。
快:单遍编译器与紧凑对象表示
Wren 的性能路线是"单遍编译到紧凑字节码 + 紧凑对象表示":
- 单遍编译器:wren_compiler.c 在 wren_opcodes.h 定义的指令集之上实现了一次扫描即产出字节码的编译器,跳过了 AST 中间环节,从源码解析到字节码一气呵成。
- 紧凑对象表示(NaN tagging):wren_value.h 中用大段注释解释了 NaN tagging 的原理:IEEE 754 双精度浮点数的 NaN 位模式有大量冗余,Wren 把这些"无意义"的位用来编码布尔值、null、堆对象指针等,而数值本身不做任何掩码处理,因此数值运算路径零开销。该注释也说明这种表示把 double、指针与若干单例值全部塞进同一个 64 位序列,从而压缩了对象体积、提升了缓存友好度。
- NaN tagging 并非硬编码,而是由编译配置开关控制:
projects/make等工程中提供了release_64bit-no-nan-tagging这类配置,可在不需要该优化时关闭。
关于性能的详细对比数据,官方另有 性能文档 阐述设计取舍;仓库的 test/benchmark 目录则收录了 fib、binary_trees、delta_blue 等经典基准的 Wren 实现(如 fib.wren、binary_trees.wren),并附有 Lua、Python、Ruby、Dart 等同题实现供横向参考。
基于类:类与继承位于语言中心
与许多"类机制不完整甚至缺席"的脚本语言不同,Wren 把类放在了前台。README 明确 "Wren places classes front and center"。仓库证据包括:
- 语法层面:类、继承、构造器都是一等语法,详见 类文档 与 方法调用文档。
- 实现层面:核心对象系统在 wren_value.c 中实现,
ObjClass等结构负责类的元模型。 - 测试层面:
test/language/class、test/language/inheritance等目录(各含 18 个用例)系统覆盖了类声明、继承、多态等场景;test/core/class下也有 equality、supertype 等行为测试。
并发:轻量 Fiber 协程作为执行模型核心
README 将 Wren 描述为 "concurrent",其实现载体是轻量协程 Fiber:所有 Wren 代码都运行在某个 fiber 内,脚本启动时自动创建 main fiber;新协程由Fiber.new {}创建,call()唤醒、Fiber.yield()挂起、transfer()切换。官方 并发文档 进一步说明:
- Fiber 是协作式调度,不会在随机时刻被抢占,开发者完全掌控切换时机,避免了线程式并发的上下文切换开销与竞态麻烦;
- Fiber 不占用操作系统线程资源,每个 fiber 只需一块栈内存,且和普通对象一样可被垃圾回收,因此可以大量创建——文档举例说游戏里可以为每个实体分配一个 fiber,交互式模式下每一行输入代码都会新建一个 fiber。
README 示例中的while (!adjectives.isDone) System.print(adjectives.call())正是 yield/call 协作的典型用法:主协程反复唤醒adjectives协程,后者每次 yield 出一个形容词,直到耗尽。仓库 test/core/fiber 下上百个.wren测试用例(call、transfer、yield 及其各种参数、错误与重入场景)构成了对这一模型最详尽的规范说明。
脚本语言:为嵌入而生,无全局状态
Wren 的最终定位是"嵌入到应用程序中运行的脚本语言",为此它:
- 只提供小型标准库和一套易用的 C API(详见下文);
- 关键设计是无全局状态:所有解释器状态都封装在
WrenVM结构体内(wren.h 注释明确 "Wren has no global state"),因此宿主可以同时创建多个互不干扰的 VM,甚至在不同线程上并发运行。
如何运行 Wren:三种上手路径
官方文档(快速上手)给出了三条路径:
- 浏览器在线体验:仓库 doc/site/try 目录是官方 Web 版的源码,包含 wren_try.wasm 与配套的 wren_try.js 前端脚本,无需安装即可运行 Wren 代码。
- Wren CLI:用于直接执行脚本的命令行程序,支持文件 IO 等能力。需要说明的是,CLI 属于独立的配套项目,本仓库只包含语言核心(VM),不含 CLI 可执行程序。
- 嵌入到自己的代码:把 Wren VM 编译成库或直接合入源码,再通过 C API 调用,这也是本仓库的核心用法,下文重点展开。
构建 Wren:工程与产物
仓库 projects 目录下按平台提供了现成的构建工程:
- Windows:打开 projects/vs2019/wren.sln(或
vs2017版本)直接编译; - macOS:打开 projects/xcode/wren.xcworkspace;
- Linux/BSD:进入 projects/make(BSD 对应 projects/make.bsd、macOS 对应 projects/make.mac)执行
make。
以 projects/make/Makefile 为例,默认配置为release_64bit,可选的config值包括:release_64bit、release_32bit、release_64bit-no-nan-tagging、debug_64bit、debug_32bit、debug_64bit-no-nan-tagging,对应产物分别为静态库wren、动态库wren_shared以及用于运行测试的wren_test。
构建完成后,库文件统一输出到仓库根目录的lib/文件夹:
| 链接方式 | Windows | Linux | macOS |
|---|---|---|---|
| 静态链接 | wren.lib | libwren.a | libwren.a |
| 动态链接 | wren.dll | libwren.so | libwren.dylib |
此外,官方还提供了"聚合构建"(amalgamated/unity build):运行python3 util/generate_amalgamation.py > build/wren.c(脚本见 util/generate_amalgamation.py)即可把全部 VM 源码合并进单个wren.c,配合 src/include/wren.h 一起加入工程即可,这是最简单的接入方式。
把 Wren 嵌入你的 C 程序:最小可运行示例
包含头文件
无论静态/动态链接还是直接编入源码,都需要把 src/include 加入头文件搜索路径,然后:
#include "wren.h"如果宿主程序是 C++ 而链接的是以 C 编译的 Wren 库,则改用 wren.hpp(它负责处理 C/C++ 调用约定差异)。Wren 只依赖 C 标准库,无需额外链接;不过 README 与文档提醒,在部分平台(如 BSD 与 Linux)上math.h的部分函数位于独立的libm库中,需要显式链接。
创建 VM:WrenConfiguration 与 wrenNewVM
创建 VM 的第一步是初始化配置结构体:
WrenConfiguration config; wrenInitConfiguration(&config);wrenInitConfiguration()会填入全部默认值(原型见 wren.h)。随后可按需覆写字段,最常用的是回调与内存参数,核心字段如下:
| 字段 | 类型 | 作用 | 默认行为 |
|---|---|---|---|
writeFn | WrenWriteFn | 接收System.print/System.write输出的文本(不含换行) | 为 NULL 时丢弃输出 |
errorFn | WrenErrorFn | 报告编译错误、运行时错误与栈帧 | 为 NULL 时不报告错误 |
loadModuleFn | WrenLoadModuleFn | 按模块名加载并返回源码(宿主负责文件 IO) | 无 |
resolveModuleFn | WrenResolveModuleFn | 把 import 字符串规范化为模块名,可用于实现相对导入 | 为 NULL 时原样使用 import 字符串 |
bindForeignMethodFn | WrenBindForeignMethodFn | 把foreign方法签名绑定到 C 函数指针 | 无 |
bindForeignClassFn | WrenBindForeignClassFn | 绑定 foreign 类的 allocate/finalize 回调 | 无 |
reallocateFn | WrenReallocateFn | 自定义内存分配/释放 | 内置realloc/free实现 |
initialHeapSize | size_t | 首次触发 GC 的堆阈值 | 0 时默认 10MB |
minHeapSize | size_t | GC 后堆的最小保留量,防止堆收缩过小导致频繁 GC | 0 时默认 1MB |
heapGrowthPercent | int | GC 后按当前存活字节的此百分比设定下一次收集阈值(例:50 表示存活 400 字节时,累计分配 600 字节触发下次 GC) | 0 时默认 50 |
userData | void* | 与 VM 关联的宿主自定义数据 | 无 |
配置就绪后创建 VM:
WrenVM* vm = wrenNewVM(&config);wrenNewVM()会拷贝一份配置(wren.h),调用后即可丢弃传入的配置结构体。由于 VM 无全局状态,多个 VM 可以完全独立运行。
执行代码:wrenInterpret 与结果码
向 VM 提交一段 Wren 源码:
WrenInterpretResult result = wrenInterpret(vm, "my_module", "System.print(\"I am running in a VM!\")");执行流程是:先把源码单遍编译为字节码,编译失败立即返回WREN_RESULT_COMPILE_ERROR;随后为这段代码新建 fiber 并运行,期间代码可自行派生更多 fiber;若运行时错误未被其他 fiber 捕获,则一路回退到主 fiber 并返回WREN_RESULT_RUNTIME_ERROR;全部 fiber 正常结束则返回WREN_RESULT_SUCCESS。所有传给wrenInterpret()的代码都运行在特殊的 "main" 模块中,因此多次调用之间可以共享顶层变量,行为类似 REPL。
释放 VM:wrenFreeVM
wrenFreeVM(vm);调用后该WrenVM*不可再使用。特别地,如果仍持有存活的WrenHandle(见 槽与句柄文档),Wren 会主动报错,防止句柄泄漏或悬空。
完整可运行示例
仓库 example/embedding/main.c 提供了上述全流程的完整实现,摘录核心部分:
#include <stdio.h> #include "wren.h" static void writeFn(WrenVM* vm, const char* text) { printf("%s", text); } void errorFn(WrenVM* vm, WrenErrorType errorType, const char* module, const int line, const char* msg) { switch (errorType) { case WREN_ERROR_COMPILE: printf("[%s line %d] [Error] %s\n", module, line, msg); break; case WREN_ERROR_STACK_TRACE: printf("[%s line %d] in %s\n", module, line, msg); break; case WREN_ERROR_RUNTIME: printf("[Runtime Error] %s\n", msg); break; } } int main() { WrenConfiguration config; wrenInitConfiguration(&config); config.writeFn = &writeFn; config.errorFn = &errorFn; WrenVM* vm = wrenNewVM(&config); const char* module = "main"; const char* script = "System.print(\"I am running in a VM!\")"; WrenInterpretResult result = wrenInterpret(vm, module, script); switch (result) { case WREN_RESULT_COMPILE_ERROR: printf("Compile Error!\n"); break; case WREN_RESULT_RUNTIME_ERROR: printf("Runtime Error!\n"); break; case WREN_RESULT_SUCCESS: printf("Success!\n"); break; } wrenFreeVM(vm); }errorFn展示了三种WrenErrorType的典型处理:编译错误携带模块名与行号、运行时错误只有消息、而栈跟踪信息则以WREN_ERROR_STACK_TRACE逐帧回调。这既是错误处理的最小实现,也对应 错误处理文档 描述的 Fiber 与错误传播机制。
更进一步的嵌入能力
VM 与宿主之间传递数据统一通过"槽"(slot)完成,宿主可用WrenHandle长期持有对 Wren 对象的引用以防被 GC 回收,详见 槽与句柄文档;foreign 类与 foreign 方法让 C 数据结构可以无缝暴露给 Wren 代码,参考 存储 C 数据 与 从 C 调用 Wren。仓库 test/api 下的 api_tests.c、slots.c、handle.c、foreign_class.c 等测试文件则是这些 API 用法的权威参考。
可选模块:Meta 与 Random
仓库 src/optional 提供了两个可选模块,它们默认编译进 VM,也可通过编译宏裁掉:
- meta(wren_opt_meta.wren):提供
Meta.eval、Meta.getModuleVariables等元编程能力,对应测试见 test/meta; - random(wren_opt_random.wren):提供随机数生成与洗牌等能力,对应测试见 test/random。
这些模块同样采用"核心用 Wren 写、性能敏感处用 C 实现"的分层策略,src/optional中每个模块都同时存在.wren与.c源码。
学习路线与测试体系
若要进一步学习,仓库 doc/site 下有一整套官方文档,建议按此顺序阅读:
- 语法文档:注释(支持嵌套块注释)、保留字(
as break class construct continue else false for foreign if import in is null return static super this true var while)、标识符等语言基础; - 变量与值、类、方法调用;
- 并发 与 错误处理:理解 Fiber 与错误传播;
- 嵌入指南 系列:配置 VM、槽与句柄、foreign 机制。
测试方面,test/core(覆盖 bool/class/fiber/function/list/map/number/string 等核心类)、test/language(覆盖 30+ 语法主题,从赋值、闭包到继承、非局部跳转)、test/api(C API 行为)、test/benchmark(性能基准)以及 test/regression(历史缺陷回归)共同构成了语言规范的"可执行文档",是深入理解 Wren 行为边界的首选素材。
结语
从 README 出发可以看到,Wren 的设计哲学高度自洽:体积小换来易读与易嵌入,单遍编译与紧凑对象表示换来性能,类机制保证了对象模型的纯粹,Fiber 协程提供了协作式并发的轻量抽象,而零依赖的 C API 让它可以安静地"住进"任何宿主程序。当前仓库对应版本 0.4.0(见 wren.h 中的版本宏)。如果这些特性符合你的需求,不妨从 示例脚本 开始体验语言本身,再按上文步骤把 VM 嵌入到自己的项目里。
- 编程语言
- 语言运行时
- 编译器
【免费下载链接】wren
The Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.
相关推荐
Wren 语言全景指南:小、快、基于类、原生并发的嵌入式脚本语言
Wren 语言全景指南:小、快、基于类、原生并发的嵌入式脚本语言 Wren 是一款面向嵌入场景的小型脚本语言,以"Smalltalk 的思想、Lua 的体积、E
编程语言语言运行时编译器Wren 语言完全指南:快速掌握小型脚本语言的终极教程
Wren 语言完全指南:快速掌握小型脚本语言的终极教程 Wren 是一种小型、快速、基于类的并发脚本语言,专为嵌入应用程序而设计。它结合了 Smalltalk
编程语言语言运行时编译器Luau脚本语言:快速嵌入与类型注解的终极指南
Luau脚本语言:快速嵌入与类型注解的终极指南 Luau是一种快速、轻量、安全且支持渐进式类型的嵌入式脚本语言,衍生自Lua。它在保持Lua简洁高效特性的基础上
编程语言语言运行时编译器解释器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考