- 编程语言
- 语言运行时
- 标准库
- 垃圾回收
- 并发编程
【免费下载链接】cangjie_runtime
仓颉编程语言运行时与标准库。
仓颉(Cangjie)的宏系统支持在编译期对源码进行语法树级变换,而std.ast包提供的 "Macro With Context"(带上下文的宏)能力,让嵌套宏之间不仅能感知彼此的调用关系,还能在展开阶段直接交换数据。本文以 stdlib/doc/libs/std/ast/ast_samples/context.md 为骨架,结合std.ast的 API 文档与 ffi.cj 源码实现,完整讲解assertParentContext的嵌套校验、setItem与getChildMessages的消息传递机制,并给出可直接运行的编译示例与排错要点。
一、背景:什么是带上下文的宏(Macro With Context)
在嵌套宏场景下,一个宏调用(内层宏)可能出现在另一个宏调用(外层宏)的调用参数内部。std.ast为这类场景提供了专门的一组上下文函数,用于在宏展开时获取调用上下文信息,主要包括:
assertParentContext(String):断言当前宏调用嵌套在指定外层宏调用内,否则报编译错误;insideParentContext(String):以布尔值形式返回嵌套关系检查结果;setItem(key, value)(重载Bool/Int64/String/Tokens):内层宏向外层宏发送键值对消息;getChildMessages(String):外层宏获取指定内层宏发送的全部消息。
这些函数有一个共同的使用限制(见 ast_package_overview.md):只能作为函数被直接调用,不能赋值给变量,不能作为实参或返回值使用。从实现上看,它们依赖宏展开器在展开过程中维护的"当前宏对象"(MACRO_OBJECT线程局部对象)来工作,因此脱离宏展开过程调用是没有意义的。
二、嵌套宏上下文校验:assertParentContext
2.1 完整示例与编译命令
宏定义文件macro_definition.cj:
// macro_definition.cj macro package macro_definition import std.ast.* public macro Outter(input: Tokens): Tokens { return input } public macro Inner(input: Tokens): Tokens { assertParentContext("Outter") return input }宏调用文件macro_call.cj:
// macro_call.cj package macro_calling import macro_definition.* main() { @Outter var a = 0 @Inner var b = 0 // error: The macro call 'Inner' should with the surround code contains a call 'Outter'. }编译命令:
# 先编译宏定义文件 cjc macro_definition.cj --compile-macro # 编译使用宏的文件 cjc macro_call.cj -o demoInner宏在定义时通过assertParentContext("Outter")声明"调用我时,我必须以@Outter宏调用为外围上下文"。由于示例调用中@Outter与@Inner是平级的两条语句、不存在嵌套关系,编译期会直接报出错误:
error: The macro call 'Inner' should with the surround code contains a call 'Outter'.2.2 源码实现视角
assertParentContext的实现位于 stdlib/libs/std/ast/ffi.cj(第 282 行附近):
public func assertParentContext(parentMacroName: String): Unit { unsafe { if (let Some(macroObjPtr) <- MACRO_OBJECT.get()) { var pStr = LibC.mallocCString(parentMacroName) ... var flag = CJ_CheckParentContext(macroObjPtr, pStr, true) ... if (!flag) { throw MacroContextException("Parent macro context check failed for macro '${parentMacroName}'.") } } } }可以看到,校验最终由运行时接口CJ_CheckParentContext完成:它会遍历宏调用上下文链,确认名为parentMacroName的外层宏调用确实包裹着当前宏。校验失败时抛出的异常类型是 MacroContextException,最终由编译器以标准错误形式呈现给用户。
2.3 需要条件判断时使用 insideParentContext
如果不想直接报错,而是想根据嵌套关系做分支处理,可以使用insideParentContext(String),它返回Bool(true表示当前宏嵌套在给定的外层宏调用内),实现同样位于 ffi.cj(第 304 行附近),其内部调用CJ_CheckParentContext(macroObjPtr, pStr, false),区别仅在于不抛出异常而是返回结果。典型用法:
public macro inner(input: Tokens): Tokens { if (insideParentContext("outer")) { // 处于 outer 宏内时的逻辑 } else { // 未处于 outer 宏内时的逻辑 } return input }三、内层宏向外层宏发送消息:setItem
assertParentContext解决的是"嵌套关系校验",而setItem解决的是"嵌套宏之间的数据通信"。
3.1 完整示例
宏定义文件macro_definition.cj:
// macro_definition.cj macro package macro_definition import std.ast.* public macro Outter(input: Tokens): Tokens { let messages = getChildMessages("Inner") for (m in messages) { let value1 = m.getString("key1") // get value: "value1" let value2 = m.getString("key2") // get value: "value2" println("value1: ${value1} value2: ${value2}") } return input } public macro Inner(input: Tokens): Tokens { assertParentContext("Outter") setItem("key1", "value1") setItem("key2", "value2") return input }宏调用文件macro_call.cj:
// macro_call.cj import macro_definition.* main() { @Outter( @Inner var cnt = 0 ) println(cnt) }编译命令:
# 先编译宏定义文件 cjc macro_definition.cj --compile-macro # 编译使用宏的文件 cjc macro_call.cj -o demo编译阶段输出:
value1: value1 value2: value2在这个例子中:内层宏Inner在展开时通过setItem("key1", "value1")与setItem("key2", "value2")把两个键值对"暂存"到外层宏的上下文中;随后外层宏Outter展开时调用getChildMessages("Inner")拿到Inner发送的一组信息对象(每个调用Inner的位置对应一条消息),再通过信息对象的getString按 key 取出对应值。
3.2 setItem 的四种重载
setItem支持四种值类型,对应四个重载(见 ast_package_funcs.md):
| 重载 | 发送类型 | 检索接口 |
|---|---|---|
setItem(String, String) | 字符串 | getString(key) |
setItem(String, Int64) | 整数 | getInt64(key) |
setItem(String, Bool) | 布尔 | getBool(key) |
setItem(String, Tokens) | 词法单元序列 | getTokens(key) |
从源码看(ffi.cj 第 331~396 行),每种重载都会把值序列化后连同类型标签交给运行时接口CJ_SetItemInfo,类型标签用常量区分:
let ITEM_TYPE_STRING: UInt8 = 1 let ITEM_TYPE_INT64: UInt8 = 2 let ITEM_TYPE_BOOL: UInt8 = 3 let ITEM_TYPE_TOKENS: UInt8 = 4例如发送Tokens时,实现会先调用value.toBytes()将词法单元序列化,再以字节指针形式传递;外层宏读取时再反序列化为Tokens。因此setItem不仅能传简单值,还能把一段代码(如quote(1 + 1))完整地传出去。
四、外层宏接收消息:getChildMessages 与 MacroMessage
4.1 接收语义
getChildMessages(children: String): ArrayList<MacroMessage>用于获取名为children的内层宏发送的全部消息。关键语义:
- 一个内层宏调用 = 一条
MacroMessage:如果Outter内调用了多次Inner,getChildMessages会返回与调用次数等长的消息列表; - 每条
MacroMessage内部是一个键值对映射,用getString/getInt64/getBool/getTokens按 key 取值; - 没有消息不是错误:从实现看(ffi.cj 第 559 行附近),当没有对应消息时返回空列表
ArrayList<MacroMessage>(),外层宏可直接遍历; - 若按 key 取不到对应类型的信息,
getString等接口会抛出Exception(如Macro item '${key}' does not exist.,或类型不匹配时提示has a different type: expected 'String')。
4.2 MacroMessage 的完整接口
MacroMessage类(见 ast_package_classes.md)除上述四个 getter 外,还提供:
hasItem(key: String): Bool:判断该条消息中是否存在指定 key,可在取值前做防御性检查。
一个使用hasItem的典型防御写法(参考 ast_package_exceptions.md 中的示例):
public macro outer(input: Tokens) { let messages = getChildMessages("inner") if (messages.size > 0 && !messages[0].hasItem("key1")) { throw MacroContextException("No such item") } return input }4.3 多值多类型综合示例
下面是在官方文档示例基础上整理的一个同时传递Int64与Bool的完整场景(宏定义与宏调用分别编译):
// 宏定义 macro package M import std.ast.* import std.collection.ArrayList public macro inner(input: Tokens) { setItem("Int64FromInner", 100) // 发送 Int64 setItem("TrueFromInner", true) // 发送 Bool return input } public macro outer(input: Tokens) { let messages = getChildMessages("inner") let msg = messages[0] println("Message from inner-macro: ${msg.getInt64("Int64FromInner")}") println("Bool message: ${msg.getBool("TrueFromInner")}") return input }// 宏调用 import M.* @outer(@inner(var a = 1)) main() { }编译运行后输出:
Message from inner-macro: 100 Bool message: true五、编译与调试流程
上下文宏涉及两类编译过程:
- 编译宏定义:使用
--compile-macro选项编译宏定义文件,例如cjc macro_definition.cj --compile-macro; - 编译宏调用方:直接编译使用宏的文件并输出可执行程序,例如
cjc macro_call.cj -o demo。
如需观察宏展开过程与中间结果,官方 API 文档中的示例还使用了--debug-macro选项(如cjc macro_call.cj --debug-macro),配合std.ast的语法树节点dump函数(示例见 ast_samples/dump.md)可以输出 AST 节点的完整结构,便于定位上下文宏代码中Tokens变换是否正确。
需要注意:宏展开后的代码不允许出现包声明和包导入语句(详见 parseProgram 的说明),因此消息传递、上下文校验等逻辑都应封装在宏函数内部完成,最终return input返回干净的可编译Tokens。
六、常见错误与排查要点
| 现象 | 原因与处理 |
|---|---|
编译报错The macro call 'Inner' should with the surround code contains a call 'Outter'. | Inner的assertParentContext("Outter")校验失败,说明调用处不存在@Outter包裹;检查调用代码的嵌套结构(参考 context.md 第一组示例)。 |
抛出Macro item 'key' does not exist. | getString等取值接口按 key 找不到对应项;先调用hasItem(key)判断,或检查setItem的 key 是否拼写一致。 |
抛出Macro item 'key' has a different type: expected 'String'. | setItem发送的类型与取值接口不匹配;setItem(String, Int64)发的值必须用getInt64取。 |
getChildMessages返回空列表 | 内层宏未发送消息或内层宏名与参数不一致;文档明确"没有消息不是错误",可按空列表处理。 |
| 上下文函数无法在宏外使用 | assertParentContext等函数依赖宏展开上下文(MACRO_OBJECT),只能在宏展开过程中直接调用。 |
总结
通过assertParentContext/insideParentContext与setItem/getChildMessages的组合,仓颉宏作者可以在编译期实现"嵌套关系约束 + 父子宏数据交换"两类能力:前者在宏定义处声明依赖的外层宏,让错误在编译期提前暴露;后者以内层宏调用为粒度,把字符串、整数、布尔甚至整段Tokens代码安全地传递给外层宏处理。配合--compile-macro、--debug-macro与dump调试手段,即可构建结构清晰、约束严谨的嵌套宏体系。相关 API 的完整签名与更多示例可继续参阅 ast_package_overview.md、ast_package_funcs.md 与源码实现 stdlib/libs/std/ast/ffi.cj。
- 编程语言
- 语言运行时
- 标准库
- 垃圾回收
- 并发编程
【免费下载链接】cangjie_runtime
仓颉编程语言运行时与标准库。
相关推荐
仓颉编程语言嵌套宏上下文通信实战:Macro With Context 的断言约束与消息传递机制
仓颉编程语言嵌套宏上下文通信实战:Macro With Context 的断言约束与消息传递机制 在仓颉(Cangjie)语言的宏系统中,嵌套宏调用(一个宏调用
文档知识库教程OpenHarmony仓颉 std.ast 包函数 API 全解析:从词法解析到宏上下文通信
仓颉 std.ast 包函数 API 全解析:从词法解析到宏上下文通信 导读 std.ast 是仓颉标准库中负责 源码解析与抽象语法树(AST)构建 的核心包,
编程语言语言运行时标准库垃圾回收并发编程仓颉 std.ast 包函数 API 全解析:从词法解析到宏上下文通信
仓颉 std.ast 包函数 API 全解析:从词法解析到宏上下文通信 std.ast 是仓颉标准库中面向编译期宏开发的语法解析包,本文以 ast_packag
文档知识库教程OpenHarmony
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考