- 编程语言
- 语言运行时
- 编译器
- 解释器
- 标准库
【免费下载链接】racket
The Racket repository
Zuo 是 Racket 官方仓库中自带的一个“微型 Racket”,它保留了#lang模块语言机制,但将实现压缩到一个可单文件编译的 C 程序,并内置文件系统、进程控制等脚本原语与一套make风格的嵌入式构建 DSL。本文以racket/src/zuo/README.md为主体,结合racket/src/zuo目录下的源码,系统讲解 Zuo 的定位、运行方式、构建安装、堆映像(heap image)加速、交叉编译、嵌入宿主应用以及其分层语言与宏系统实现,帮助读者把它用作轻量脚本语言或 Racket 自身的构建工具。
Zuo 是什么:为什么需要“更小的 Racket”
Zuo(名称源自中文“做/作”,即“make”之意,见 overview.scrbl)的定位很明确:通常情况下你应该用 Racket 写脚本;但当你需要一个远比 Racket 小的运行时,或者要“脚本化 Racket 自身的构建”时,Zuo 就是答案。它是一个带make风格嵌入式 DSL 的微型 Racket,内置了文件处理与进程运行原语。
Zuo 是 Racket 的“变体”体现在:程序文件以#lang开头,#lang之后的模块路径决定文件内容的解析与展开方式——make风格的 DSL 正是通过这种方式定义的,连基础 Zuo 语言本身也是由一层层#lang叠加而成,其中早期的一层就实现了宏。这一点在源码中得到印证:lib/zuo/base.zuo 只有寥寥几行——它从zuo/kernel起步,用module->hash拿到语言构造器后生成基础语言;而 lib/zuo/private/base/main.zuo 则直接include了base-common的实现。
需要强调的是,尽管程序以#lang开头,Zuo 拥有完全独立的实现,Zuo 程序并不能通过 Racket 运行(详见 overview.scrbl)。
运行 Zuo 的几种方式
Zuo 可执行文件只运行模块,运行入口的规则如下(README 原文 + overview.scrbl 补充):
| 启动方式 | 行为 |
|---|---|
| 无命令行参数 | 加载当前目录下的main.zuo |
| 第一个参数是文件路径 | 运行该文件(须以#lang开头) |
| 第一个参数是目录 | 运行该目录下的main.zuo |
-c标志 | 第一个参数被当作模块文本直接运行(1.1 版本加入) |
第一个参数为空字符串"" | 从标准输入读取模块文本(空串不是合法文件路径,因而被特殊解释) |
额外的参数通过runtime-env过程传递给目标 Zuo 程序。./zuo build install就等价于运行build/main.zuo并传入参数install。无论最初运行哪个脚本,只要该脚本带有main子模块(用module+声明),该子模块也会被运行。
特别地,zuo .与zuo ./main.zuo等价,因此zuo .可以作为make的便捷替代——仍然能传递参数。
从示例脚本入门
README 推荐了四个示例,其中两个非常简单,适合快速上手:
hello.zuo(racket/src/zuo/local/hello.zuo)展示了最小程序:
#lang zuo "Hello, world!" ;; 若不想要引号: ;; (alert "Hello, world!") ;; 若不想显示为蓝色: ;; (displayln "Hello, world!")文件仅以#lang zuo开头,随后一个字符串字面量就是整个程序;alert(高亮显示)与displayln是带输出的替代写法。
tree.zuo(racket/src/zuo/local/tree.zuo)是一个功能完整的tree命令实现,示范了 Zuo 脚本的典型结构:
- 顶层
(provide tree)把函数作为库导出; (module+ main ...)声明main子模块,当脚本作为主程序运行时解析命令行参数并调用tree;command-line宏模拟 Racket 的command-line形式,但因为没有状态,必须显式把累加器accum在线程中传递——:init (hash)初始化累加器,:once-each声明每个标志至多出现一次,[accum ("-a") "帮助文本" (hash-set accum 'all? #t)]为标志-a绑定处理动作,:args ([dir "."])声明位置参数;(hash-ref (runtime-env) 'script)取回当前脚本路径用于错误信息。
同时该示例展示了 Zuo 的算术局限:只支持 64 位有符号整数,所以human-readable函数只能自己用quotient/modulo做“K/M”单位换算(源码注释直言“Arithmetic is not Zuo's strong suit”)。目录内容通过ls与stat获取,用glob->matcher(见 lib/zuo/glob.zuo)过滤点文件,build-path拼接路径。
构建与安装 Zuo
直接编译
最简方式:用 C 编译器编译zuo.c(racket/src/zuo/zuo.c)即可,除系统头文件与 C 库头文件外不需要任何额外文件,也不需要特殊编译标志,但-o zuo、-O2之类是推荐的。这个“单文件可编译”的事实并非宣传——zuo.c是完整的自包含实现(本仓库中该文件约 7679 行,文件头注释即说明“kernel 实现尽量分层”),其中约包含 100 个内核原语。
configure / make 方式
也可以使用configure、make、make install:make目标在编译zuo.c之后基本都会调用一个 Zuo 脚本。Makefile.in 展示了这一机制:
zuo: $(srcdir)/zuo.c $(CC_FOR_BUILD) $(CFLAGS_FOR_BUILD) $(CPPFLAGS_FOR_BUILD) \ -DZUO_LIB_PATH='"$(srcdir)/lib"'"'"'" -o zuo $(srcdir)/zuo.c $(LDFLAGS_FOR_BUILD) $(LIBS_FOR_BUILD) check: zuo ./zuo . to-run/zuo to-run/zuo . check注意zuo:目标编译时通过-DZUO_LIB_PATH指定库集合路径;而check:则是“先编译 zuo,再用 zuo 去构建 to-run/zuo,再用 to-run/zuo 去跑测试”,典型地演示了自举式的 Zuo 构建流程。
如果你不使用configure,而是把zuo.c直接编译到当前目录的zuo,那么./zuo build.zuo与./zuo build.zuo install(Windows 上省略./)等价于默认配置下的make与make install。
无参数运行时的提示
如果直接运行./zuo(无参数),它会加载当前目录下的 main.zuo,后者会用alert打印一段欢迎信息,提示你“没有参数时默认加载 main.zuo”,并说明如何传参、如何用""直接输入程序。
make风格构建 DSL 与 build.zuo 剖析
Zuo 的核心卖点之一是自带的make风格 DSL,而 Zuo 自己就用它来构建自己。build.zuo 是一个完整的实例:
- 开头
(require "local/image.zuo")复用image.zuo中导出的image-target; (provide-targets targets-at)导出构建目标生成函数targets-at,它接受构建目录与变量哈希表,返回目标列表;- 配置来源:若存在
configure生成的Makefile,则用config-file->hash读取;否则按平台取默认值——Unix 默认prefix=/usr/local、CC=cc、CFLAGS=-O2,Windows 默认prefix=C:\Program Files\Zuo、CC=cl.exe、CFLAGS=/O2; target形式定义目标:(target 输出路径 (lambda (path token) (rule 依赖列表 重建函数))),依赖中的quote-module-path表示“本脚本自身”,input-data-target表示一段配置数据;- 定义了两个可执行目标:
to-run/zuo(把lib目录的相对路径编译进二进制,便于就地运行)与to-install/zuo(内嵌安装后的库路径); phony-rule定义伪目标:zuos-to-run-and-install作为默认目标同时构建两个可执行文件;check伪目标运行测试套件 tests/main.zuo;install伪目标处理DESTDIR(用于打包暂存)、mkdir-p建目录、cp/cp*复制可执行文件与lib/zuo库目录(macOS 上先rm旧文件)。
测试套件本身也是 Zuo 脚本:tests/main.zuo通过require链拉入 30 多个测试模块(equal、integer、hash、process、filesystem、macro、make、build、form 等),全部通过后(alert "... tests passed!")结束——这与check目标的(process-status ...)校验、失败即(error "check failed")的机制配合,构成了 Zuo 的自测闭环。
库集合与启动性能
除内建语言模块zuo/kernel外,Zuo 通过库集合(collection of libraries)查找语言与模块。默认情况下,Zuo 以可执行文件所在目录的相对路径lib作为库集合树根;可用-X命令行标志指定其他集合路径。在zuo.c中对应ZUO_LIB_PATH宏(默认"lib")与ZUO_LIB_PATH环境变量:zuo_configure()会先检查环境变量再回落到编译期默认值,且#define EMBEDDED_IMAGE 0是 local/image.zuo 生成嵌入代码时锚定的标记行。
堆映像(heap image):消除启动开销
你可以创建一个把一组库作为堆映像(heap image)嵌入的 Zuo 实例,两个优势:
- 无需额外的库模块目录;
- Zuo 启动极快,可与最快的命令行程序相竞争。
local/image.zuo 生成一个.c文件,它是zuo.c的副本加上嵌入的模块。其核心机制在embed-image函数中可见:
- 默认嵌入
'zuo模块及其依赖,可用++lib <module-path>指定更多(多次出现即累加); - 生成副本时默认禁用默认集合根路径,除非给
local/image.zuo传--keep-collects; - 生成方式:以管道方式启动一个子进程 Zuo,向其写入
#lang zuo/kernel代码,用module->hash装载指定库,最后调用dump-image-and-exit把镜像写到 stdout; - 随后逐行扫描
zuo.c,找到#define EMBEDDED_IMAGE 0那一行替换为#define EMBEDDED_IMAGE 1,并把镜像字节以embedded_boot_image[]数组嵌入 C 源码;--keep-collects缺失时还会追加#define ZUO_LIB_PATH NULL禁用库路径; - 命令行参数还包括
-o <file>(输出文件)、--image <file>(直接嵌入已有镜像文件而非新建)、--deps <file>(记录构建依赖,供增量构建的 up-to-date 判断); image-target函数把同一套配置封装为构建目标,供build.zuo使用,其重建函数通过build/dep把实际读过的模块文件登记为增量依赖。
当你使用configure与make(或./zuo build.zuo)时,默认构建目标创建嵌入zuo库的to-run/zuo,以及to-install/zuo——后者在make install/./zuo build.zuo install之后有正确的内部路径去查找其他库。
不嵌入的堆映像:-B/--boot
堆映像也可以不嵌入:Zuo 内核原语dump-heap-and-exit创建堆映像(包含所有已装载模块),-B或--boot命令行标志让 Zuo 启动时使用给定引导映像;也可以把dump-image-and-exit创建的镜像通过local/image.zuo --image嵌入。无论独立文件还是嵌入.c源码,引导映像都是机器无关的。
交叉编译
使用./configure --host=...交叉编译时,还需要给./configure添加类似CC_FOR_BUILD=cc的参数,为构建机(build machine)上的zuo指定编译器;必要时还可指定CFLAGS_FOR_BUILD、LDFLAGS_FOR_BUILD与LIBS_FOR_BUILD。这些变量在 configure.ac 中通过AC_ARG_VAR声明(另有CPPFLAGS_FOR_BUILD),未显式提供时默认取$(CC) -O2等宿主值,并随AC_CONFIG_FILES写入生成的Makefile与main.zuo。
configure.ac还提供了两个与嵌入相关的开关:
--enable-embed=<libs>:指定嵌入哪些库,默认"zuo",no表示不嵌入(对应EMBED_LIBS="");--enable-big:在zuo之外再嵌入zuo/hygienic。
最终EMBED_LIBS经AC_SUBST进入生成的Makefile,被 build.zuo 中的(hash-ref config 'EMBED_LIBS "zuo")读取并传给image-target。
把 Zuo 嵌入到更大的应用
Zuo 可以嵌入更大的应用,带或不带内嵌引导映像均可。做法是:用ZUO_EMBEDDED预处理宏(定义为任意值)编译zuo.c或local/image.zuo的输出;这种情况下会用到zuo.h头文件,嵌入方应用也应使用zuo.h。嵌入 API 的文档以注释形式写在 zuo.h 中。
zuo.h定义了完整的三步启动协议:
zuo_ext_primitive_init():初始化原语,之后可用zuo_ext_add_primitive(proc, arity_mask, name)添加自定义原语。注意:为保证镜像可用,原语必须始终按相同顺序添加,镜像只在“具有相同原语集合”的环境中有效;zuo_ext_image_init(char *boot_image_file):装载引导映像;传NULL使用默认或内嵌映像。此后可用zuo_ext_false/true/null/void/eof/empty_hash取常量,用zuo_ext_integer/cons/string/symbol/hash_ref/hash_set构造与访问数据,用zuo_ext_kernel_env()取内核环境(但此时还不要装载、求值或使用任何模块);zuo_ext_runtime_init(lib_path, runtime_env):确定runtime-env与库查找路径,lib_path传#f可禁用库加载,runtime_env哈希表作为runtime-env结果的起点('exe、'args、'script等键需自行放入)。此后全部功能可用:用zuo_ext_eval_module从文本求值模块,用zuo_ext_apply调用过程。
由于zuo_ext_t*值在求值期间可能被垃圾回收或移动,跨求值保存值必须用zuo_ext_stash_push/zuo_ext_stash_pop。zuo.h内置两个#if 0的完整示例:一个用#lang zuo模块经dynamic-require取main后调用并打印(+ 1 2)的结果;另一个注册返回 5 的random-five原语并用#lang zuo/kernel模块消费它。ZUO_EXT_APPLY_ALWAYS_EXPECTS_VALUES宏记录了 1.2 版之前zuo_ext_apply的一个修复(此前对非原语过程会误把过程与实参当表达式求值)。
分层语言与两套宏系统
zuo/kernel(由zuo.c实现)是一种语法极简的语言,配约 100 个原语过程。Zuo 面向脚本,因而文件系统、I/O、进程原语很重——按 overview.scrbl 的统计,约一半原语服务于这类任务,另有约 1/3 原语用于数字、字符串与哈希表。
语言层叠顺序(README 称“基础 Zuo 语言由一层层#lang叠加而成,早期一层实现宏”),结合lib/zuo/private/目录可还原为:
zuo/kernel:zuo.c内建的最小语言;- looper 层(
looper.zuo):补充简化版letrec、cond、let*; - stitcher 层(
stitcher.zuo):looper 的唯一使用者,加入自带隐式begin的lambda、多子句let、let*、多绑定子句letrec、and、or、when、unless及一种define和include; zuo/private/base/base-common:基础语言实现,包含and-or、define、let、opt-lambda、quasiquote、struct、syntax-error、check-dups等模块(目录 racket/src/zuo/lib/zuo/private/base 与base-common、base-hygienic并列);zuo/base(lib/zuo/base.zuo):由base-common的make-language组装;zuo:#lang zuo的最终语言,附带zuo/build等库。
宏系统有两套实现,源码基本同源,只是以“作用域与绑定的表示”参数化(结合zuo/datum与 stitcher 层的include实现):
zuo使用的非卫生宏:作用域模型表达能力较弱(写“生成宏的宏”时才会察觉差别),但快得多,对多数脚本场景足够;zuo/hygienic提供的卫生宏:基于与 Racket 相同的 set-of-scopes 模型。
zuo与zuo/hygienic模块可以在一个程序里混用,但不能在一个语言里使用另一个语言的宏。更一般地,Zuo 定义了#lang协议,允许从字符/字节层面构建任意新语言,只要最终能用zuo/kernel表达。
Zuo 的数据类型与模块协议
内核支持的数据类型(见 overview.scrbl):布尔;64 位二进制补码整数(模运算);字节字符串(可选#前缀与\n、\r、\t、\"、\\、八进制转义);符号(分常驻与非常驻);列表;以符号为键的持久化映射哈希表(内部其实不真正做哈希);过程(包括以过程形式实体化的一等续延);variable(有名字、一次性设置、单值容器);opaque 对象(键值对,只有持键者能取到值,键通常靠词法作用域保密);handle(文件、进程等系统资源句柄)。值得注意的缺失:浮点数、字符、Unicode 字符串、向量。路径用字节字符串表示(Windows 宽字符路径隐含 UTF-8 编码)。
模块在核心层面就是一张哈希表,不同语言层可以自由赋予键含义;核心模块系统只对一个键'read-and-eval赋予语义——它是实现#lang语言的模块提供的三参数过程(模块文本字符串、#lang与语言名之后正文起点位置、映射到求值结果的模块路径),返回代表求值后模块的哈希表。module->hash原语负责按需装载:先检查已装载,未装载则读取源开头解析#lang得到语言模块路径,递归装载语言模块后以其'read-and-eval处理源文本。启动序列还赋予第二个键'submodules语义(键到 thunk 的映射),Zuo 运行初始脚本时查找'main子模块并调用其 thunk。zuo、zuo/base、zuo/hygienic语言约定'dynamic-require键映射到dynamic-require函数(1.2 版加入),用于访问导出的值。
路径处理的设计约定
脚本与构建系统绕不开路径问题,Zuo 做了几个明确取舍(README 未展开,但 overview.scrbl 的“Path Handling”一节有完整说明):
- 依赖语法规范化:
a/b之上构建../c得到a/c;若a/b是指向x/y/z的目录符号链接,文件系统会解出a/x/y/c,与 Zuo 的结果不同——目录符号链接会与 Zuo 的路径函数产生分歧,文件符号链接无此问题,文档建议干脆别用目录链接; - 进程内工作目录不可变:相对路径因此在更多场景下可靠,系统调用保持相对形式交给操作系统解析;启动子进程时可指定其工作目录,此时需用
find-relative-path手动调整传给子进程的相对路径; - 尽量使用并传播相对路径:脚本启动方式影响结果——
zuo scripts/go.zuo启动时quote-module-path报相对路径,zuo /home/.../go.zuo则报绝对路径;zuo/build记录依赖信息时同样遵循“相对引用记相对、绝对引用记绝对”; - source/build 目录显式分离:
zuo/build鼓励显式区分“源目录”与“构建目录”,二者都不必是当前工作目录,加上工作目录不变,共同促成可组合的构建脚本。
深入阅读:文档与测试
- 安装 racket/src/zuo/zuo-doc 目录为 Racket 包,即可渲染 Zuo 的正式文档(
lang-zuo.scrbl、lang-zuo-kernel.scrbl、lang-zuo-hygienic.scrbl、lang-zuo-datum.scrbl、reader.scrbl、zuo-lib.scrbl、overview.scrbl等); - 完整测试套件位于 racket/src/zuo/tests,入口 tests/main.zuo 覆盖内核、语法、宏、构建 DSL、文件系统、进程、镜像等;
- 语言分层实现位于 racket/src/zuo/lib/zuo/private,库模块(cmdline、glob、shell、config、c、jobserver、thread 等)位于 racket/src/zuo/lib/zuo。
综上,Zuo 用约 100 个内核原语加一层层#lang语言,在“单文件 C 实现 + 自举的 make 风格构建 DSL + 可嵌入的堆映像”三者间取得了独特的平衡:它既是 Racket 仓库(racket/src/zuo)的自举构建工具,也是一个可独立分发、可嵌入宿主程序、启动速度可与最快速命令行程序竞争的微型脚本语言。
- 编程语言
- 语言运行时
- 编译器
- 解释器
- 标准库
【免费下载链接】racket
The Racket repository
相关推荐
如何使用Xmake Lua脚本扩展:定制你的构建逻辑全指南
如何使用Xmake Lua脚本扩展:定制你的构建逻辑全指南 Xmake是一款基于Lua的跨平台构建工具,它允许开发者通过Lua脚本轻松扩展和定制构建逻辑。本文将
开发工具构建工具CLI包管理器Zephyr RTOS构建系统:链接器脚本定制
Zephyr RTOS构建系统:链接器脚本定制 为什么需要定制链接器脚本? 在嵌入式开发中,内存资源往往受限且架构差异大。Zephyr RTOS作为一款面向多硬
操作系统嵌入式RTOS物联网突破构建限制:Cocos Creator自定义构建脚本全指南
突破构建限制:Cocos Creator自定义构建脚本全指南 你是否还在为Cocos Creator构建流程中的重复操作而烦恼?每次打包都要手动压缩资源、修改配
游戏开发图形学3D渲染
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考