ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Wren 模块系统完全指南:import 语法、模块定位与循环依赖解析

Wren 模块系统完全指南:import 语法、模块定位与循环依赖解析 编程语言语言运行时编译器【免费下载链接】wrenThe Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.项目地址https://gitcode.com/gh_mirrors/wr/wren点击查看免费下载Wren 是一门小巧、快速、基于类的并发脚本语言而模块系统是它组织代码、实现复用的核心机制。本文以官方文档 doc/site/modularity.markdown 为主体结合仓库源码与测试用例系统讲解 Wren 模块的定位、执行、变量绑定三大步骤以及共享导入、循环依赖、嵌入自定义加载器等进阶主题。读完本文你将能熟练编写多文件 Wren 程序、正确设计模块间的依赖关系并在嵌入场景中实现自己的模块加载策略。为什么需要模块从单文件到多文件一旦程序规模超过小玩具级别开发者会很快遇到两个问题拆分希望把程序分解成多个更小的文件方便在代码中快速定位复用希望在不同程序之间复用部分代码。Wren 用一套简单的模块系统解决这两个问题一个包含 Wren 代码的文件就定义了一个module模块。一个模块可以通过import导入使用另一个模块中定义的代码。大程序可以拆成多个被导入的小模块多个程序也可以共享同一个模块实现代码复用。需要特别强调的是Wren 没有单一全局作用域。每个模块拥有独立于其他模块的顶层作用域。这意味着两个模块可以定义同名顶层变量而不会发生命名冲突——每个模块都是真正模块化的。import 语句快速入门当你运行 Wren 并传入一个文件名时该文件的内容定义了执行起始的 main 模块。要加载并执行其他模块使用 import 语句import beverages for Coffee, Tea这条语句的工作方式是先找到名为 beverages 的模块并执行其源码然后在那个模块中查找两个顶层变量Coffee和Tea并在本模块中创建持有它们值的新变量。import 语句可以出现在任何允许变量声明的地方甚至可以出现在块block内部if (thirsty) { import beverages for Coffee, Tea }重命名导入与无绑定导入如果需要以不同名字导入变量使用import ... for Name as OtherName语法——它在目标模块中查找顶层变量Name但在本模块中声明名为OtherName的变量来持有其值import liquids for Water // Water 这个名字现在被占用了 import beverages for Coffee, Water as H2O, Tea // var water H2O.new()如果只想加载模块、不绑定其中任何变量可以省略for子句import some_imperative_code这种形式常用于加载带有副作用如初始化逻辑的模块。导入的三个步骤整体来看一次导入被分解为三个明确步骤定位Locate找到模块的源码执行Execute运行被导入模块的代码绑定Bind在导入模块中把新变量绑定到被导入模块中定义的值上。下文将逐一深入这三个步骤并结合 VM 源码src/vm/wren_vm.c讲解其底层实现。定位模块从名字到源码导入的第一步是真正找到模块的代码。import 指定一个name——一个用于唯一标识模块的任意字符串。宿主应用程序embedding application控制着如何使用这个字符串定位源码这是 Wren 模块系统最灵活的设计之一。嵌入场景自定义模块加载器当宿主应用创建 Wren VM 时它提供一个模块加载函数WrenConfiguration config; config.loadModuleFn loadModule; // 其他配置... WrenVM* vm wrenNewVM(config);该函数的签名定义在 src/include/wren.h 中typedef WrenLoadModuleResult (*WrenLoadModuleFn)(WrenVM* vm, const char* name);每当模块被导入时VM 都会调用该函数并传入模块名。嵌入方需要在WrenLoadModuleResult中返回该模块的源码内容。嵌入方可以按任何方式处理访问文件系统、查找打包在应用内的资源都可以。WrenLoadModuleResult结构体src/include/wren.h包含三个字段source模块的源码字符串若模块未找到则返回NULLonComplete可选的完成回调Wren 处理完结果后会调用它便于释放内存userData任意用户数据。如果在source字段返回NULLWren 会将其报告为运行时错误。这在 VM 源码中对应Could not load module .错误信息见 src/vm/wren_vm.c。内建可选模块meta 与 random如果宿主没有提供源码VM 会进一步检查是否请求了内建的可选模块。在importModule的实现中src/vm/wren_vm.c当加载函数返回空时VM 会检查模块名是否为meta或random并从对应的内建源码src/optional/wren_opt_meta.wren、src/optional/wren_opt_random.wren中加载。这两个模块通过编译期宏WREN_OPT_META和WREN_OPT_RANDOM控制是否启用。命令行加载器基于相对路径的查找Wren CLI 命令行工具的查找过程非常简单它把模块名和 .wren 追加到主模块所在目录后查找文件。例如运行$ wren code/my_program.wren且主模块包含import some/module则命令行 VM 会尝试查找/code/some/module.wren。按照约定即使是在 Windows 上也应使用正斜杠作为路径分隔符以确保脚本跨平台可移植正斜杠在 Windows 上是合法分隔符而反斜杠在其他操作系统上不合法。仓库示例 example/import_module/lovecraft.wren 中的import ./cthulu for Cthulu就是相对导入的实际用法。执行模块暂停、运行、恢复拿到模块源码后需要运行它。首先VM 会暂停正在执行 import 语句的那个 fiberWren 的协程并发原语。然后创建一个新的模块对象——基本上就是一个全新的顶层作用域——以及一个新的 fiber在新 fiber 和新作用域中执行新模块的代码。模块可以运行任意命令式代码也可以定义任意顶层变量。当模块代码执行完毕、其 fiber 完成时被挂起的导入方模块的 fiber 会被恢复。这种挂起与恢复是递归的如果 a 导入 b、b 又导入 c那么 a 和 b 都会在 c 运行期间被挂起。c 完成后恢复 bb 完成后恢复 a。可以把这想象成逐个节点遍历 import 树——在任意时刻只有一个模块的代码在运行。从源码看这个流程的核心在importModulesrc/vm/wren_vm.c先解析模块名再检查模块注册表随后调用加载器取源码、编译模块并返回模块闭包最后在解释器循环中执行。绑定变量快照而非实时连接模块执行完毕后最后一步是从中真正导入数据。任何模块都可以定义顶层变量top-level variables——即在任何 method 或 function 之外声明的变量。它们对模块内部一切代码可见同时也可以被导出给其他模块使用。当 Wren 执行import beverages for Coffee, Tea时它先运行 beverages 模块然后遍历for子句中的每个变量名。对每个名字它在该模块中查找同名顶层变量如果找不到就是运行时错误。否则取得该变量的当前值并在导入模块中定义同名同值的新变量。快照语义不是活连接这里有一个值得注意的关键语义导入模块得到的是自己的变量其值是被导入变量在导入时刻值的快照。如果之后任一模块给该变量重新赋值另一个模块不会看到变化——这不是实时连接。不过在实践中大多数顶层变量只被赋值一次所以这种差别很少造成影响。在 VM 源码中这对应getModuleVariable函数src/vm/wren_vm.c它通过wrenSymbolTableFind在模块的variableNames符号表中查找变量名若找到variableEntry ! UINT32_MAX则返回module-variables.data[variableEntry]中存储的值否则抛出Could not find a variable named in module .的运行时错误。测试用例 test/language/module/change_imported_value/ 专门验证了这种导入值改变不影响已导入方的快照行为而 test/language/module/unknown_variable/ 则验证了查找不到变量时的报错路径。共享导入模块只执行一次前面把程序的模块集合描述成一棵树。当然只有不存在共享导入时它才是一棵真正的树。考虑这样的程序// main.wren import a import b // a.wren import shared // b.wren import shared // shared.wren System.print(Shared!)这里 a 和 b 都想使用 shared。如果 shared 定义了某些顶层状态我们只希望内存中有一份拷贝。为处理这种情况模块的代码只在第一次被加载时执行。之后再次导入该模块只是查找之前已加载的模块。内部实现上Wren 维护了一个已加载模块的映射表vm-modules。当一个模块被导入时Wren 先在这个映射表中查找然后才调用嵌入方获取源码。这对应importModule中的这一行逻辑src/vm/wren_vm.cValue existing wrenMapGet(vm-modules, name); if (!IS_UNDEFINED(existing)) return existing;换句话说前面的三步步骤之外还隐含着第零步看看我们是否已经加载过这个模块如果加载过就复用它。因此上面的程序只会打印一次 Shared!。测试用例 test/language/module/shared_import/shared_import.wren 的注释明确写着 Shared module should only run once并通过expect注释断言了输出顺序a、shared、a done、b、b done——shared 只在 a 导入时执行一次。循环导入小心但可行只要稍微注意导入甚至可以有环。加载过程详细来说是看看是否已经创建了给定名字的模块如果是直接使用它否则创建具有该名字的新模块并存入模块注册表为该模块创建 fiber 并执行其代码。注意最后两步的顺序模块在加载时先被加入注册表然后才执行。这意味着如果在该模块自身或其某个导入正在执行期间又遇到了针对同一模块的 import它会在注册表中被找到从而短路循环。例如// main.wren import a // a.wren System.print(start a) import b System.print(end a) // b.wren System.print(start b) import a System.print(end b)这个程序可以成功运行并打印start a start b end b end a循环导入中的变量绑定陷阱需要小心的是绑定变量。考虑// main.wren import a // a.wren import b for B var A a variable // b.wren import a for A var B b variableb.wren 中对 a 的导入会失败。追踪执行过程在 main.wren 中执行import a挂起 main.wren在 a.wren 中执行import b挂起 a.wren在 b.wren 中执行import a。由于 a 已在模块映射表中不会挂起它转而查找名为A的变量——但此时 a.wren 还停在import b for B那一行A尚未声明因此查找失败。要让其工作需要把变量声明移到 import 之上// main.wren import a // a.wren var A a variable import b for B // b.wren import a for A var B b variable现在执行流程变为在 main.wren 中执行import a挂起 main.wren在 a.wren 中定义A在 a.wren 中执行import b挂起 a.wren在 b.wren 中执行import a。由于 a 已在模块映射表中不挂起它查找已定义的A并绑定在 b.wren 中定义B完成 b.wren在 b.wren 中查找B并绑定到 a.wren恢复 a.wren。这听起来非常麻烦但循环依赖本来就普遍麻烦。关键点是在极少数需要它们的情况下Wren 可以处理循环依赖。仓库中 test/language/module/cyclic_import/cyclic_import.wren 就是这一行为的回归测试其expect注释精确断言了完整的输出序列start a → a defined a value → start b → b defined b value → b imported a value → end b → a imported b value → end a印证了上面描述的绑定顺序。提前退出模块顶层 return虽然return语句通常用于从 method 或 function 退出它也可以用在模块的顶层代码中提前退出模块。例如如果脚本由单个模块组成以下代码会提前退出模块因此也退出脚本for (i in 1..2) { if (i 2) return System.print(i) // 打印 1 但不打印 2 } System.print(3) // 不会执行到虽然返回一个值并非非法但没有途径访问那个值因此它会被直接丢弃。测试目录 test/language/module/returns/ 下的多个用例如return_from_import.wren、return_value_from_import.wren专门验证了从导入模块顶层 return 的行为。深入从 RFC 到实现——嵌入 API 的演化如果你查阅 doc/rfc/0001-smarter-imports.md会发现模块系统经历过一轮设计演进。该 RFC 的注释说明它现已基本实现只是实现与最初提案略有不同。与本文主题最相关的落地成果是嵌入 API 增加了解析回调typedef const char* (*WrenResolveModuleFn)(WrenVM* vm, const char* importer, const char* name);它定义于 src/include/wren.h。按照头文件注释src/include/wren.h在导入加载之前VM 会调用它传入包含导入的模块名和导入字符串宿主应用可以据此生成一个新的规范canonical字符串来唯一标识模块。该字符串之后被用作模块名传给loadModuleFn、用于检测同一模块的重复导入、并出现在栈跟踪中。如果该函数保持NULL则原始导入字符串直接作为规范字符串回退到旧行为。在 VM 侧resolveModulesrc/vm/wren_vm.c从当前执行帧的闭包模块取得导入方名字调用resolveModuleFn并对返回结果做字符串拷贝管理若解析函数返回NULL则产生Could not resolve module imported from .的运行时错误。常用模块相关测试与示例一览以下是仓库中可直接对照学习的模块相关测试与示例基本导入test/language/module/simple_import/重命名导入astest/language/module/import_as/块内导入test/language/module/inside_block/多变量导入test/language/module/multiple_variables/相对路径导入test/language/module/relative_import/共享导入只执行一次test/language/module/shared_import/循环导入test/language/module/cyclic_import/模块顶层 returntest/language/module/returns/错误场景找不到模块 test/language/module/unknown_module.wren、找不到变量 test/language/module/unknown_variable/、缺少for子句 test/language/module/missing_for.wren嵌入示例example/import_module/lovecraft.wren 与 example/import_module/cthulu.wren 演示了import ./cthulu for Cthulu的相对导入写法通过本文的梳理你可以清晰把握 Wren 模块系统的完整脉络import语句的三步执行模型、模块注册表驱动的共享导入语义、先注册后执行所支持的循环依赖以及通过loadModuleFn/resolveModuleFn两个嵌入回调实现的完全可定制的模块定位策略。这套设计让 Wren 既能作为命令行解释器直接运行多文件脚本也能作为嵌入式脚本语言融入宿主应用的资源管理体系中。赞分享编程语言语言运行时编译器【免费下载链接】wrenThe Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.项目地址https://gitcode.com/gh_mirrors/wr/wren点击查看免费下载相关推荐palera1n 越狱 iPad mini 4手把手完整操作指南palera1n 越狱 iPad mini 4手把手完整操作指南 palera1n 是专为 A8~A11 老设备打造的开源越狱工具利用 bootROM 里的CLI固件CoffeeScript 模块系统完全指南import / export 语法、动态导入与自动 bare 模式CoffeeScript 模块系统完全指南import / export 语法、动态导入与自动 bare 模式 CoffeeScript 从 2.0 起完整支编程语言编译器10分钟音频训练声音模型RVC WebUI 开源AI声音克隆入门指南10分钟音频训练声音模型RVC WebUI 开源AI声音克隆入门指南 想让直播或配音使用某个特定目标音色最直接的路径是准备10分钟左右的干净录音训练一个专人工智能AI 应用语音音频深度学习上一篇10 分钟跑通 MAS 激活Windows 与 Office 激活的 4 种方式完整流程下一篇5分钟掌握抖音批量下载神器从零构建高效素材管理生态创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表