news 2026/9/11 20:59:20

Mojo 生命周期、origin 与引用(ref)实战:从 lifetime checker 到容器引用安全

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Mojo 生命周期、origin 与引用(ref)实战:从 lifetime checker 到容器引用安全

Mojo 生命周期、origin 与引用(ref)实战:从 lifetime checker 到容器引用安全

【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo

导读

本文以仓库中 Mojo/docs/site/code/manual/values/lifetimes/README.md 所指向的 Mojo Manual 的 Lifetimes, origins, and references 章节 及其配套代码示例为主线,系统讲解 Mojo 编译器的生命周期检查机制:什么是 origin(起源)、如何用ref参数与ref返回值表达参数化可变性的引用,以及SpanPointer等按 origin 参数化的类型如何与容器安全协作。读完本文,你将能看懂并写出带显式 origin 与ref的 Mojo 类型签名,理解origin_of()ImmStaticOrigin、origin 并集等核心概念,并掌握如何在 Bazel 工程中编译运行这些示例。

代码示例目录的定位

仓库中的Mojo/docs/site/code/manual/values/lifetimes/目录是 Mojo Manual 中 "Lifetimes, origins, and references" 一节的配套代码与测试集。目录结构与约定如下:

  • 每个.mojo文件都是一个独立、可单独运行的 Mojo 应用程序;
  • BUILD.bazel 为每个.mojo文件生成一个mojo_binary目标(以去扩展名的文件名命名),并为每个二进制目标生成一个modular_run_binary_test测试目标(以_test后缀命名)。

目前目录包含三个示例文件,分别覆盖了 origin 与引用主题的三个侧面:

文件主题
name_list.mojoref返回值:__getitem__()返回可变引用并直接就地修改
immutable_name_list.mojoref返回值的参数化可变性:不可变入参自动退化为只读引用
inferred_origin.mojo推断 origin:用Origin[mut=is_mutable]参数把入参 origin 传递到返回的Span

从 BUILD.bazel 可以看到其构建方式:

MOJO_SRCS = glob(["*.mojo"]) [ mojo_binary( name = src.split(".")[0], srcs = [src], deps = ["@mojo//:std"], ) for src in MOJO_SRCS ] [ modular_run_binary_test( name = src.split(".")[0] + "_test", size = "small", binary = src.split(".")[0], ) for src in MOJO_SRCS ]

即:所有.mojo文件自动成为二进制目标,并各配一个small大小的运行测试。这意味着每个示例不仅是文档片段,更是被持续集成验证的可执行程序。

理解 origin:编译器追踪生命周期与引用有效性的核心

Mojo 编译器内置了一个生命周期检查器(lifetime checker)——一个分析程序数据流的编译器 pass。它判断变量在哪些时刻仍然有效,并在变量生命周期结束时自动插入析构调用(deinitializer calls)。

编译器使用一种特殊值origin来追踪变量的生命周期与引用的有效性。具体而言,一个 origin 回答两个问题:

  1. 哪个变量"拥有"这个值?—— 决定引用的存活期间需要延长谁的寿命;
  2. 通过这个引用能否修改该值?—— 决定可变性(mutability)与独占性检查的语义。

考虑以下代码(取自 lifetimes.mdx):

def print_str(s: String): print(s) def main(): var name: String = "Joan" print_str(name)

name = "Joan"声明了一个带标识符(name)和一块String逻辑存储空间的变量。当把name传入print_str()时,函数拿到的是对该值的不可变引用。于是names指向同一块逻辑存储空间,各自关联的 origin 值让编译器能够对它们进行统一的推理。

关键认知:origin 追踪发生在编译期。origin 并不追踪name变量实际分配的那块存储地址,而是符号化地追踪变量——编译器记录"print_str()是用调用方作用域中由name拥有的值调用的"。通过追踪被拥有的数据如何在程序中流动,编译器就能确定值的生命周期。

大多数情况下,origin 由编译器自动处理。但在以下两类场景中,你需要直接与 origin 打交道:

  • 使用引用时——尤其是ref参数与ref返回值;
  • 使用以 origin 为类型参数的类型时——例如PointerSpan,它们都以所指向数据的 origin 作为参数化依据。

Origin 类型:ImmOriginMutOriginOrigin

Mojo 提供了一个Origin结构体以及一组comptime类型别名,用于在签名中显式书写 origin 类型。见 Mojo/stdlib/std/origin/init.mojo:

comptime ImmOrigin = Origin[mut=False] # 不可变 origin 引用类型 comptime MutOrigin = Origin[mut=True] # 可变 origin 引用类型

Origin[mut=...]结构体携带一个mut: Bool参数,表示该 origin 是可变的、不可变的,还是"可变性取决于外围 API 给定的参数"。

例如,固定不可变的引用类型可以这样声明:

struct ImmutRef[origin: ImmOrigin]: pass

而参数化可变性的引用类型则写成:

struct ParametricRef[ is_mutable: Bool, //, origin: Origin[mut=is_mutable] ]: pass

这里的is_mutable是一个infer-only(仅推断)参数——调用方无法显式指定,只能由编译器根据实参推断。origin参数同样常常是被推断的。例如下面的代码创建了一个指向既有值的Pointer,但无需书写 origin——origin 自动从既有值推断而来:

from std.memory import Pointer def use_pointer(): var a = 10 var ptr = Pointer(to=a)

从源码看,Origin结构体(Mojo/stdlib/std/origin/init.mojo)本身带有两个参数:mut: Bool以及一个原始 MLIR origin 值(_mlir_origin),并且继承自TrivialRegisterPassable——这印证了 origin 是编译期符号、可像寄存器值一样传递,且不产生运行时表示

Origin 集(OriginSet)

OriginSet并不是一种 origin 类型,而是一组 origin 的集合,用于追踪参数化闭包(parametric closures)中捕获值的生命周期。需要强调的是,OriginSet不是表达"多个 origin 组合"的通用机制——表达多 origin 组合应使用下面的origin_of()并集形式。对应到 MLIR 层面,它是!lit.origin.set这一单例类型(见 Mojo/stdlib/std/origin/init.mojo 与 origin-design.md)。

Origin 值的五种来源

大多数 origin 值由编译器创建。作为开发者,有几种方式可以获得或指定 origin 值:

1. 静态 origin:ImmStaticOrigin

ImmStaticOrigin代表存活于整个程序运行期间的不可变值。字符串字面量(StringLiteral)就拥有ImmStaticOrigin。例如as_string_slice()返回一个指向原始字符串字面量的StringSpan——字符串字面量在编译期分配、永不销毁,因此该切片使用不可变静态 origin。

2. 派生 origin:origin_of()

origin_of(value)是一个魔法操作符,返回传入值(或值列表)关联的 origin。它的参数可以是任意求值结果满足以下条件之一的表达式:

  • 一个 origin 值;
  • 一个具有内存位置(memory location)的值。

例如:

origin_of(self) origin_of(x.y) origin_of(foo())

origin_of()纯静态的编译期分析——传给它的表达式永远不会被真正求值(例如编译器分析origin_of(foo())时并不会执行foo()函数)。

下面的BoxedString结构体展示了派生 origin 的典型用法:as_ptr()返回一个Pointer,其 origin 与原OwnedPointer相同:

from std.memory import OwnedPointer, Pointer struct BoxedString: var o_ptr: OwnedPointer[String] def __init__(out self, value: String): self.o_ptr = OwnedPointer(value) def as_ptr(mut self) -> Pointer[String, origin_of(self.o_ptr)]: return Pointer(to=self.o_ptr[])

注意as_ptr()self采用mut self约定。若使用默认参数约定,self就是不可变的,派生出的origin_of(self.o_ptr)也会变成不可变 origin——参数的 mutability 会通过origin_of()传导到返回类型上。

origin_of()还可以接收多个表达式以表达 origin 的并集:origin_of(a, b)

3. 推断 origin

由于 origin 本质上是参数,编译器可以根据传入函数的实参推断origin 值(即"参数推断"机制)。这使得函数能够返回一个与传入参数同 origin 的值——ref参数一节将给出具体示例。

4. 未追踪 origin:MutUntrackedOriginImmUntrackedOrigin

未追踪 origin 表示不与任何既有值别名的值:它们指向不被任何其他变量拥有的内存,因此生命周期检查器不追踪它们。例如alloc()返回一个指向新分配动态内存块的Allocation,其 origin 为MutUntrackedOrigin——表示这块内存不受 Mojo 所有权系统管理。使用此类不安全 API 时,寿命管理责任在开发者自己:例如一个分配内存的结构体,通常应在析构函数中释放该内存。

从 Mojo/stdlib/std/origin/init.mojo 的实现可以看到,未追踪 origin 正是#lit.origin.union<>——一个空并集,承诺该引用不别名任何编译器管理的值。

5. 通配 origin:ImmUnsafeAnyOriginMutUnsafeAnyOrigin

通配 origin 是特殊情形,表示可能访问任何存活值的引用。它们曾被广泛用于不安全指针。携带通配 origin 的指针进入某作用域后,会带来三重副作用:

  • 只要指针存活,就停用该作用域内所有值的 ASAP(尽可能早)析构;
  • 阻止 Mojo 强制执行参数独占性检查;
  • 隐藏未使用变量的警告。

因此,通配 origin 的使用被明确劝阻,应作为最后手段。源码注释同样强调这是"早期 Mojo 时代的临时编译器逃生舱,永不稳定,计划弃用并移除"(Mojo/stdlib/std/origin/init.mojo)。

使用引用:ref参数与ref返回值

ref关键字可用于参数与返回值,表达参数化可变性(parametric mutability)的引用——即引用本身在调用点既可能是可变的也可能是不可变的。ref返回值对调用方而言看起来与普通返回值无异,但它是对既有值的引用而非拷贝

ref参数

ref参数约定让你指定一个可变性参数化的参数:无需事先知道传入实参是可变的还是不可变的。使用ref参数通常出于以下原因:

  • 希望接受参数化可变性的参数;
  • 希望把某个参数的寿命绑定到另一个参数的寿命;
  • 希望参数保证以内存形式传递:这对需要"身份"的参数化类型很有用,无论具体类型是否可以按寄存器传递。

ref参数的语法有两种形式:

ref arg_name: arg_type

或带 origin 子句的形式:

ref[origin_specifier(s)] arg_name: arg_type

第一种形式下,ref参数的 origin 与可变性从传入值推断;第二种形式在方括号内给出一个或多个 origin 说明符。origin 说明符可以是:

  • 一个 origin 值

  • 任意表达式,视为origin_of(expression)的简写——以下两种声明等价:

    ref[origin_of(self)] ref[self]
  • 一个AddressSpace值;

  • 下划线_,表示 origin 是未绑定的(等价于省略 origin 说明符):

    def add_ref(ref a: Int, b: Int) -> Int: return a+b

你也可以显式命名 origin。这在想把参数限制为ImmOriginMutOrigin之一、或想把函数返回值绑定到某个参数的 origin 时非常有用。

实战示例:把 origin 从参数传递到返回值

Span是连续数据的非拥有视图(如字符串的子串、列表的子集)。因为它指向自己不拥有的数据,所以以 origin 值参数化,该 origin 代表其所指向数据的寿命与所有权。

to_byte_span()接收一个List[Byte],返回一个与列表同 origin 的Span[Byte](该示例正是目录中的 inferred_origin.mojo):

from std.collections import List, Span def to_byte_span[ is_mutable: Bool, //, origin: Origin[mut=is_mutable], ](ref[origin] list: List[Byte]) -> Span[Byte, origin]: return Span(list) def main(): var list: List[Byte] = [77, 111, 106, 111] _ = to_byte_span(list)

这里的origin参数由list实参推断而来,并被用作返回Span的 origin。由于Span继承了list实参的 origin,编译器可以识别该 span 的数据由list拥有:span 与 list 有相同的寿命,且 list 可变时 span 也可变

从设计文档 origin-design.md 可知,这种"无 origin 说明符的ref参数"在解析时会获得一个推断 origin:parser 为函数添加两个隐式参数(一个Bool表示可变性、一个!lit.origin<mut=该bool>表示 origin 本身),并把参数的!lit.ref类型绑定到它们。这就是单个ref a: Int签名无需两个重载就能同时接受example(1)example(someMutVar)的底层原理。

ref返回值

ref参数类似,ref返回值允许函数返回对某个值的可变或不可变引用。其语法为:

-> ref[origin_specifier(s)] arg_type

注意:ref返回值必须提供 origin 说明符。允许的说明符取值与ref参数一致。

ref返回值是高效处理集合元素更新的常用手段。标准做法是实现__getitem__()__setitem__()这对 dunder 方法——它们分别在下标读取与写入时被调用:

var value = list[a] list[b] += 10

__getitem__()使用ref参数/返回值时,它可以返回一个可直接就地修改的可变引用。与__setitem__()相比各有取舍:

  • 可变引用更高效——单次更新不会被打散成两个方法调用;但代价是被引用的值必须位于内存中。
  • __getitem__()/__setitem__()成对方案则允许在取值与赋值时运行任意代码,例如__setitem__()可以对输入值做校验或约束。
示例:返回引用的__getitem__()

以下NameList__getitem__()返回一个引用(即目录中的 name_list.mojo):

from std.collections import List struct NameList: var names: List[String] def __init__(out self, *names: String): self.names = List[String]() for name in names: self.names.append(name) def __getitem__(ref self, index: Int) raises -> ref[self.names[0]] String: if index >= 0 and index < len(self.names): return self.names[index] else: raise Error("index out of bounds") def main() raises: var list = NameList("Thor", "Athena", "Dana", "Vrinda") ref name = list[2] print(name) name += "?" print(list[2])

输出:

Dana Dana?

这里用ref name = list[2]创建了一个引用绑定(reference binding),随后name += "?"直接修改了list中下标 2 的元素。注意返回值类型中的ref[self.names[0]]——它把返回引用的 origin 派生自self.names的首元素 origin,从而将引用与NameList内部的存储正确关联。

重要区分:如果把ref返回值赋给普通变量,变量得到的只是被引用项的拷贝;只有使用引用绑定语法才能捕获引用供后续使用:

var name_copy = list[2] # owned copy of list[2] ref name_ref = list[2] # reference to list[2]

返回值的参数化可变性

ref返回值的另一大优势是支持参数化可变性。回到__getitem__()的签名:

def __getitem__(ref self, index: Int) raises -> ref[self] String:

由于返回值的 origin 绑定在self的 origin 上,当方法通过可变引用调用时返回可变引用;而当你只有NameList的不可变引用时,方法依然可用,只是返回不可变引用(即目录中的 immutable_name_list.mojo):

def pass_immutable_list(list: NameList) raises: print(list[2]) # list[2] += "?" # Error, this list is immutable def main() raises: var list = NameList("Sophie", "Jack", "Diana") pass_immutable_list(list)

输出:

Diana

注释行list[2] += "?"若被取消注释将触发编译错误——因为此处list是不可变参数。若没有参数化可变性,你就必须为__getitem__()编写两个版本:一个接受不可变self,另一个接受可变self。这正是ref机制的价值所在。

带 origin 并集的返回值

ref返回值的 origin 说明符中可以包含多个值,从而表达 origin 的并集(union)。例如下面的pick_one()返回两个输入字符串之一的引用,其 origin 是两者 origin 的并集:

def pick_one(cond: Bool, ref a: String, ref b: String) -> ref[a, b] String: return a if cond else b

由于编译器无法静态确定运行时会走哪个分支,该函数必须使用并集 origin[a, b]。这确保编译器在返回引用存活的整个期间同时延长两个值的寿命。并集引用只有在所有组成 origin 都可变时才可变(对应设计文档中的规则:并集的可变性是各成员可变性的逻辑 AND)。

从 origin-design.md 的 IR 层面描述可以看到,并集在 MLIR 中表现为#lit.origin.union<...>属性;CheckLifetimespass 在引用存活期间会延长所有成员 origin 的寿命,这正是"生命周期扩展(lifetime extension)"不变量的一部分。

底层实现:origin 如何穿过编译管线

虽然本目录以代码示例为主,但理解 origin 的底层生命周期有助于写出更可靠的类型签名。根据仓库中的设计文档 origin-design.md,origin 信息经过三个阶段:

Mojo source ▼ Parser (MojoParser) ── 生成带 origin 的 LIT IR;检查调用点独占性 ▼ LowerSemanticCF ── 为分析降低语义控制流 ▼ CheckLifetimes ── 检查未初始化变量使用、插入析构调用 ▼ LowerLIT ── lit → kgen;剥离所有 origin 类型/属性 ▼ Elaboration, code generation, etc...
  • Parser负责构建绝大多数 origin:为函数签名、局部变量、ref参数等附加 origin 属性,并在每个调用点执行参数独占性检查(读/读别名允许,读/写、写/写冲突在同一 origin 上被拒绝)。
  • CheckLifetimes是生命周期检查核心,强制执行四条不变量:使用先于销毁(引用不得比其命名的值存活更久)、生命周期扩展(被引用值存活到最后一个引用消失,包括并集 origin)、参数独占性ASAP 析构(在最早安全点插入析构调用)。
  • LowerLIT在寿命验证完成后把!lit.ref<T, origin>降低为普通指针!kgen.pointer<T>,所有 origin 属性被替换为空结构体——origin 仅存在于编译期,零运行时开销

该文档还详细描述了 interior origin(内部 origin)机制:List等容器把元素存放在堆存储中,list[i]返回的引用指向容器拥有的内存。编译器通过 interior origin(如诊断信息中渲染的list["element"])对流敏感的引用失效建模——append()等可能重分配缓冲区的操作会使先前取得的元素引用失效,后续使用将报编译错误,而不是像 C/C++ 那样成为未定义行为。这也解释了为什么NameList.__getitem__的返回类型需要显式书写ref[self.names[0]]这类 origin 派生表达式:它把返回引用的寿命与容器的具体存储槽位正确挂钩,让编译器能够精确追踪失效关系。

如何构建与运行这些示例

仓库采用 Bazel 构建。要在本地编译运行本目录的示例与测试,可以基于 Mojo/docs/site/code/manual/values/lifetimes/BUILD.bazel 的规则定义,使用仓库根目录的bazelw包装脚本执行类似命令:

./bazelw build //Mojo/docs/site/code/manual/values/lifetimes:name_list ./bazelw test //Mojo/docs/site/code/manual/values/lifetimes:name_list_test

name_list可替换为immutable_name_listinferred_origin等任一示例名。)每个示例的mojo_binary目标依赖@mojo//:std(即标准库),modular_run_binary_test目标负责把可执行文件跑一遍作为冒烟测试。

此外,本目录的权威文字说明位于 Mojo/docs/site/manual/values/lifetimes.mdx,而更深入的编译器内部设计(LIT IR、origin 属性代数、interior origin)可继续阅读 Mojo/proposals/origin-design.md;origin 类型的标准库定义与全部别名(ImmOriginMutOriginImmUntrackedOriginMutUntrackedOriginImmStaticOriginOriginSet等)集中在 Mojo/stdlib/std/origin/init.mojo。

小结

Mojo 的 origin 系统把"值归谁拥有"与"能否通过此引用修改"两个问题提升为编译期一等公民:Origin[mut=...]提供参数化的可变性类型,origin_of()提供派生 origin 与并集表达,ref参数与ref返回值则让 API 既能参数化地接受或返回引用,又能把寿命关系从入参精确传递到出参。仓库中三个配套示例从"可变引用就地修改"、"不可变自动退化"到"origin 跨函数传递"层层递进,配合 Bazel 构建测试,是动手验证上述概念的最小可运行集。掌握这些机制,是写出既安全又高效的容器与指针类 Mojo API 的基础。

【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 20:59:19

GIS三维分析中的栅格插值技术与ArcToolbox实战

1. 项目概述&#xff1a;栅格插值在三维分析中的核心价值在GIS三维分析领域&#xff0c;栅格插值技术就像魔术师手中的变形工具&#xff0c;能将离散的点数据转化为连续的空间表面。作为ArcToolbox中3D Analyst模块的看家本领&#xff0c;这套工具链解决了地质勘探、环境监测等…

作者头像 李华
网站建设 2026/9/11 20:57:40

智驾芯片选型实战指南:算力、确定性与生态成本三维决策

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 20:56:37

使用 MLflow h2o Flavor 管理 H2O 模型的完整指南

使用 MLflow h2o Flavor 管理 H2O 模型的完整指南 【免费下载链接】mlflow The open source AI engineering platform for agents, LLMs, and ML models. MLflow enables teams of all sizes to debug, evaluate, monitor, and optimize production-quality AI applications wh…

作者头像 李华
网站建设 2026/9/11 20:55:41

基于深度学习1DCNN的轴承故障诊断:从振动信号到端到端分类实践

简介&#xff1a;基于深度学习的1DCNN轴承故障诊断源码包&#xff0c;面向机械故障诊断、工业预测性维护领域的工程师与研究人员&#xff0c;提供从振动信号预处理、1DCNN模型构建、训练优化到故障分类的完整实现方案。资源共50个文件&#xff0c;包体仅3.64MB&#xff0c;以Py…

作者头像 李华