第 3 章 第一个 Mojo 程序与 CLI 全解
本章目标:把一个 Mojo 程序从源码走到可执行文件、汇编、LLVM IR、共享库;把
mojo的 8 个子命令逐个用一遍;最后写一个真正有用的命令行工具(wc)。本章所有输出都是在 Linux x86-64 + Mojo 1.2.0 上的真实终端记录。
3.1 程序的结构:main() 与顶层声明
每个 Mojo 可执行程序都必须有一个名为 main() 的函数作为入口:
mojo
def main():
print("Hello, world!")
output
Hello, world!
注意几件事:
main()不需要raises,除非它内部调用了可能抛错的函数且不打算自己处理(见第 10 章)。print()本身不是 raising 函数。- 顶层不能有可执行语句。这是很多 Python 转 Mojo 的人第一个撞上的墙:
mojo
var x = 10 # 错误:global variables are not supported
print("hi") # 错误:expressions must not appear at file scope
编译器会明确告诉你:
output
error: global variables are not supported; move this into a function body
or use 'comptime' to declare a constant
顶层(文件作用域)只允许声明 :def、struct、trait、import、from ... import,以及 comptime 常量。
- 顶层常量用
comptime:
mojo
comptime MAX_SIZE = 1024
comptime Greeting = "hello"
comptime Vec8 = SIMD[DType.float32, 8]
def main():
print(MAX_SIZE, Greeting)
var v = Vec8(1.0)
print(v.reduce_add())
output
1024 hello
8.0
对比 Python :Python 的模块顶层可以跑任意代码(这就是为什么
import有副作用)。Mojo 的顶层是纯声明式的------这让编译期分析、增量编译、AOT 优化都成为可能。需要"模块加载时执行"的逻辑?放到main()或显式的init()函数里。
3.2 mojo run:最快的迭代循环
bash
mojo run hello.mojo # 显式
mojo hello.mojo # 等价简写
run = 编译到临时产物 + 立即执行。它是开发期的主力命令。
关键选项
| 选项 | 说明 |
|---|---|
-O0 / -O1 / -O2 / -O3 |
优化级别,默认 3 ;--no-optimization ≡ -O0 |
-I <PATH> |
追加模块搜索路径(可多次) |
-D <KEY=VALUE> |
定义编译期常量,源码里用 std.sys.is_defined 查询 |
-g / -g0/-g1/-g2 |
调试信息级别:none / line-tables / full |
-j <N> |
编译并行线程数(默认 0 = 全部核心) |
--emit <KIND> |
产出别的东西而不是可执行文件(见 3.4) |
开发期技巧 :编译大型泛型代码时
-O3可能很慢。先用-O0或-O1快速验证逻辑,最后再-O3测性能。这是 Mojo 版的"debug build / release build"。
参数传递的顺序陷阱
bash
mojo run -O2 app.mojo --port 8080 --verbose
# └── 给编译器 ──┘└──── 给 app 程序 ────┘
**文件路径之前的选项给 mojo,之后的原样传给你的程序。**在程序里这样读:
mojo
import std.sys
def main() raises:
var args = std.sys.argv()
print("argc =", len(args))
for i in range(len(args)):
print(t" argv[{i}] = {args[i]}")
bash
mojo run args.mojo --port 8080 --verbose
output
argc = 3
argv[0] = --port
argv[1] = 8080
argv[2] = --verbose
⚠️ 注意:
mojo run时argv[0]是第一个程序参数 ,不是可执行文件名(与mojo build出的二进制不同,后者的argv[0]是程序路径)。写健壮的 CLI 时不要依赖argv[0]的含义,从索引 0 开始当作纯参数列表处理,或用mojo build后的二进制。
3.3 实战:写一个 wc(行数/词数/字节数统计)
这是一个麻雀虽小五脏俱全的 CLI 程序:读命令行参数、打开文件、遍历 UTF-8 码点、格式化输出、支持编译期开关。
mojo
import std.sys
from std.io.file import open
from std.sys import is_defined
def pad_left(s: String, width: Int) -> String:
var out = String()
var n = width - Int(s.count_codepoints())
for _ in range(max(n, 0)):
out += " "
out += s
return out
def count_file(path: StringSpan) raises -> Tuple[Int, Int, Int]:
var lines = 0
var words = 0
var chars = 0
with open(path, "r") as f:
var text = f.read()
chars = text.byte_length()
var in_word = False
for cp in text.codepoints():
var c = Int(cp)
if c == 10:
lines += 1
if c == 32 or c == 10 or c == 9:
in_word = False
elif not in_word:
in_word = True
words += 1
return lines, words, chars
def main() raises:
comptime if is_defined["VERBOSE"]():
print("[verbose build]")
var args = std.sys.argv()
if len(args) < 2:
print("usage: wc <file> [<file> ...]")
return
var tl = 0
var tw = 0
var tc = 0
for i in range(1, len(args)):
var l, w, c = count_file(args[i])
print(pad_left(String(l), 7), pad_left(String(w), 7),
pad_left(String(c), 7), args[i])
tl += l
tw += w
tc += c
if len(args) > 2:
print(pad_left(String(tl), 7), pad_left(String(tw), 7),
pad_left(String(tc), 7), "total")
保存为 wc.mojo,准备两个测试文件后运行:
bash
printf 'hello world\nthis is a test\nthird line here\n' > sample.txt
printf 'a b c d e\nf g\n' > sample2.txt
mojo run wc.mojo sample.txt sample2.txt
真实输出:
output
3 9 43 sample.txt
2 7 14 sample2.txt
5 16 57 total
这段 40 行的程序里已经用到了本书后面会详细讲的六个特性:
| 代码 | 特性 | 章节 |
|---|---|---|
with open(...) as f |
上下文管理器 / RAII | 第 10 章 |
raises / main() raises |
错误传播 | 第 10 章 |
StringSpan |
非拥有字符串视图 | 第 23 章 |
text.codepoints() |
UTF-8 感知的迭代 | 第 23 章 |
Tuple[Int, Int, Int] + 解包 |
多返回值 | 第 5 章 |
comptime if is_defined[...] |
编译期开关 | 第 18 章、26 章 |
一个刻意的"坑":为什么不用 t"{l:>7}"?
Python 程序员会本能地写 f"{lines:>7}"。在 Mojo 里这会编译失败:
mojo
def main():
var l = 42
print(t"{l:>7}")
output
error: format specifiers are not supported in t-strings;
format the value manually before interpolating
TString(模板字符串)不支持格式说明符 。同样,String.format() 也只支持位置索引,不支持 : 后的对齐/宽度/精度:
mojo
def main():
print("{:>7}".format(42))
output
note: constraint failed: Index :<7 not in kwargs
format() 支持的只有这两种:
mojo
def main():
print("{0} {1} {0}".format("Mojo", 1.125))
print("{} {}".format(True, "hello"))
output
Mojo 1.125 Mojo
True hello
所以本书的 wc 里手写了 pad_left()。这是 Mojo 当前的真实状态:字符串格式化能力弱于 Python,需要自己补。第 23 章会给出一套可复用的格式化辅助函数。
3.4 mojo build:产出可交付物
bash
mojo build -O3 -o wc wc.mojo
./wc sample.txt
实测:
output
$ mojo build -O3 -o wc wc.mojo
$ ls -la wc
-rwxr-xr-x 1 user user 75752 ... wc
$ ./wc sample.txt
3 9 43 sample.txt
75 KB 的静态可执行文件,无 Python 依赖(除非你用了 Python 互操作------那么运行环境必须提供相应的 Python 库,官方文档明确提醒:"Beware that any Python libraries used in your Mojo project are not included in the executable binary")。
--emit:不产出可执行文件,产出中间产物
mojo build --emit <KIND> 是理解 Mojo 编译流水线的最佳窗口。实测支持的类型(括号内是支持该类型的目标):
--emit |
产物 | 支持目标 |
|---|---|---|
exe(默认) |
可执行文件 | amdgpu, host, metal, nvptx |
asm |
目标汇编;GPU 目标还会为每个 kernel 产出 sidecar 文件 | amdgpu, host, metal, nvptx |
llvm |
未优化的 LLVM IR;GPU 同样有 sidecar | amdgpu, host, metal, nvptx |
llvm-bitcode |
LLVM bitcode (.bc) |
amdgpu, host, metal, nvptx |
object |
单个目标文件(实验性) | amdgpu, host, metal, nvptx |
shared-lib |
动态库 | amdgpu, host, metal, nvptx |
air |
Apple AIR bitcode(每 kernel 一个)+ host LLVM bitcode | metal |
试试:
bash
mojo build --emit asm -O3 wc.mojo -o wc.s
mojo build --emit llvm -O1 wc.mojo -o wc.ll
实测 wc.s 约 330 KB(含全部内联展开),wc.ll 约 23,781 行。看一眼 LLVM IR 的开头:
output
; ModuleID = 'wc.mojo'
source_filename = "wc.mojo"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
@static_string_a8d4ace0dc8d360e = internal constant [2 x i8] c" \00", align 16
这是排查性能问题的第一手材料 :第 31 章会教你用 --emit asm 检查热点循环是否真的向量化了。
--emit shared-lib:给别的语言用
mojo
@export("add_one")
def add_one(x: Int) abi("C") -> Int:
return x + 1
注意 abi("C"):@export 要求函数显式声明 C ABI 效果,否则只是警告,但符号调用约定可能不对。
bash
mojo build --emit shared-lib -O3 lib1.mojo -o libmylib.so
nm -D --defined-only libmylib.so | head
实测输出:
output
0000000000001100 T add_one
一个干净的 C ABI 符号,可以被 C/C++/Python(ctypes)/Rust 直接调用。第 29 章详解。
目标平台选项(交叉编译)
bash
mojo build --print-effective-target # 当前会用哪个目标
mojo build --print-supported-targets # 支持哪些目标三元组
mojo build --print-supported-cpus # 支持哪些 CPU
mojo build --print-supported-accelerators # 支持哪些加速器
实测 --print-effective-target:
output
Effective target configuration:
--target-triple x86_64-unknown-linux-gnu
--target-cpu cascadelake
--print-supported-accelerators 的开头:
output
Supported Accelerator Architectures:
#
AMD (ROCm/HIP):
gfx90a - CDNA2 (MI250X)
...
相关选项:--target-triple、--target-cpu、--target-features、--target-abi、--march、--mcpu、--mtune、--target-accelerator。第 26 章讲交叉编译工作流,第 32 章讲 GPU 目标。
浮点行为控制:--fp-mode
bash
mojo build --fp-mode contract=off -O3 numerics.mojo
--fp-mode 接受 feature=value 列表(可多次给出),目前只有一个 feature:contract。
contract=fast(默认 ):类似 Clang 的-ffp-contract=fast,会把a + b*c融合成 FMA,跨语句也融合,因此破坏严格 IEEE 一致性;contract=off:关闭收缩。
这在数值代码里非常重要 :默认开启的 FMA 收缩会让你的结果和"逐运算舍入"的参考实现有细微差异。做精度对齐(比如把 Mojo kernel 的结果和 PyTorch 逐位比对)时,先试
contract=off。第 31、34 章会反复用到。
其他值得知道的 build 选项
| 选项 | 用途 |
|---|---|
-D KEY=VALUE |
编译期常量(配合 std.sys.is_defined / get_define) |
--sanitize <CHECK> |
打开 sanitizer(实验性 );配 --shared-libasan |
--Werror / --Wno-error |
警告升级为错误 / 取消 |
--warn-on-unstable-apis |
对每个用到的不稳定 API 报警告 |
--ignore-deprecated <NAME> |
临时忽略某个弃用告警(迁移期有用) |
--experimental-fixit / --experimental-export-fixit <YAML> |
输出机器可读的自动修复建议(这是给工具/CI 用的,1.0 迁移就是靠它) |
--disable-builtins |
不加载 prelude(写 freestanding/裸机代码时用) |
--diagnose-missing-doc-strings |
缺 docstring 就报警告 |
--max-notes-per-diagnostic <N> |
限制每条诊断附带的 note 数量(复杂报错会刷屏) |
-Xlinker <ARG> |
透传参数给链接器 |
--lld-path <PATH> |
指定 lld 路径 |
--diagnostic-format <FORMAT> |
诊断输出格式(CI 集成用) |
--print-cache-location |
打印 .mojo_cache 位置 |
3.5 mojo repl:交互式探索
bash
mojo # 等价于 mojo repl
REPL 是学习语言和验证 API 的最佳工具。实测会话:
output
$ printf 'var x = 40\nprint(x + 2)\ndef f(a: Int) -> Int:\n return a * 2\nprint(f(21))\nvar v = SIMD[DType.float32, 4](1,2,3,4)\nprint(v.reduce_add())\n' | mojo repl
42
42
10.0
(SIMD[.float32, 4]) v = [1, 2, 3, 4]
(Int) x = 40
几个观察:
var声明会回显类型和值 :(Int) x = 40。这在你不确定类型推断结果时非常有用。- 多行
def直接写就行 ,不需要 Python REPL 那种...续行提示。 - 标准库自动可用(
SIMD、print、List等无需 import)。
⚠️ 踩坑记录 :在某些精简 Linux 上,
mojo repl会报
outputerror while loading shared libraries: libncurses.so.6: cannot open shared object file因为 REPL 依赖 LLDB 的 readline 前端。解决:
apt-get install libncurses6(Debian/Ubuntu)或yum install ncurses-libs。
REPL 里最有价值的用法是探 API:
mojo
var s = String("hello world")
print(s.byte_length())
print(s.count_codepoints())
print(s.split(" "))
不确定某个方法存不存在?在 REPL 里试比翻文档快。
3.6 mojo format 与 mblack:代码风格
Mojo 有两个格式化入口:
mojo format------ 内置的 lint/format 工具:
bash
mojo format --help
output
DESCRIPTION
Formats the given set of Mojo sources using a Mojo-specific lint tool.
OPTIONS
Format options
--line-length <INTEGER>, -l <INTEGER>
Sets the max character line length. Default is 80.
mblack------ 独立可执行文件,"Mojo 版 black",随mojo包一起安装:
bash
cat ugly.mojo
output
def f( a:Int,b:Int )->Int :
return a+b
bash
mblack --diff ugly.mojo
output
--- ugly.mojo
+++ ugly.mojo
@@ -1,2 +1,2 @@
-def f( a:Int,b:Int )->Int :
- return a+b
+def f(a: Int, b: Int) -> Int:
+ return a + b
would reformat ugly.mojo
All done! ✨ 🍰 ✨
1 file would be reformatted.
bash
mblack ugly.mojo # 原地格式化
cat ugly.mojo
output
def f(a: Int, b: Int) -> Int:
return a + b
风格约定(与 black 一致):4 空格缩进、80 列(可调)、运算符两侧空格、逗号后空格、双引号字符串。
建议在项目里加 pre-commit hook:
yaml
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: mblack
name: mblack
entry: mblack
language: system
types_or: [file]
files: \.mojo$
docstring 也会被检查 :Mojo 的 lint 会警告"docstring 摘要句应以
.、!、?或反引号结尾"。用中文写 docstring 时,句号。会触发这个警告------不影响编译,但 CI 里开--Werror时会失败。要么用英文 docstring,要么在 CI 里放行这类警告。
3.7 mojo doc:从 docstring 生成结构化文档
mojo
"""A tiny demo module."""
def add(a: Int, b: Int) -> Int:
"""Return the sum of two integers.
Args:
a: First addend.
b: Second addend.
Returns:
The sum a + b.
"""
return a + b
bash
mojo doc docdemo.mojo
输出是 JSON(不是 HTML):
output
{
"decl": {
"aliases": [],
"description": "",
"functions": [
{
"kind": "function",
"name": "add",
"overloads": [
{
"args": [
{
"convention": "imm",
"description": "First addend.",
"kind": "argument",
"name": "a",
"passingKind": "pos_or_kw",
"path": "/std/simd/#int",
"type": "Int"
},
...
要点:
- 官方明确说这是"early version",只产出结构化 JSON,不生成 HTML,格式可能变。
- 输入可以是文件或目录(目录会递归处理所有模块,产出单个 JSON)。
-o <PATH>指定输出文件,不给就写 stdout。- JSON 里能看到参数的
convention(这里是imm,即默认的只读借用约定)------这是理解 Mojo 参数约定的绝佳材料(第 12 章)。
docstring 的规范(Args: / Returns: / Raises: / Parameters: / Constraints: 等段落)见 docstring reference,本书第 26 章会给出完整模板。
3.8 mojo precompile:预编译包
bash
mojo precompile src/mylib -o mylib.mojopkg
把一整个包编译成 .mojopkg,下游项目 import 它时不用重新编译源码。这是 Mojo 的二进制分发单位(因为 ABI 不稳定,分发的是"预编译的 Mojo IR"而不是机器码)。
典型用法:
bash
# 库作者
mojo precompile src/my_mojo_lib -o $PREFIX/lib/mojo/my_mojo_lib.mojopkg
# 使用者
mojo build -I $PREFIX/lib/mojo app.mojo
第 26 章讲完整的打包发布流程(含 recipe.yaml + rattler-build)。
3.9 mojo debug:调试器
bash
mojo debug app.mojo # 启动 CLI 调试器(默认 -g full)
mojo debug 封装了 LLDB,并加载 Mojo 的数据可视化器(lldb-visualizers),所以 print 一个 List[Int] 会显示成人类可读的形式而不是原始内存。
也支持外部编辑器:
bash
mojo debug --editor vscode app.mojo # 生成 VS Code launch 配置并启动
典型 LLDB 会话:
output
(lldb) b main.mojo:12
(lldb) run
(lldb) p x
(lldb) bt
(lldb) n
⚠️ 官方文档坦承:"Please note that there are issues when generating debug info for some Mojo programs that have yet to be addressed. " 遇到调试信息异常时,试试
-g1(只生成行号表)而不是-g2/full。
第 27 章有完整的调试实战(含条件断点、内存检查、core dump 分析)。
3.10 mojo demangle:还原符号名
Mojo 的符号名经过 mangling(因为泛型要编码参数)。看到一堆乱码符号时:
bash
mojo demangle '_ZN4mojo4mainEv'
配合 nm / objdump 使用:
bash
nm -C libmylib.so # -C 自动 demangle
nm libmylib.so | awk '{print $3}' | mojo demangle
第 29 章讲 C FFI 时会大量用到(@export("name") 就是为了绕过 mangling)。
3.11 八个子命令速查
| 命令 | 一句话 | 何时用 |
|---|---|---|
mojo run <f> |
编译并运行 | 日常开发 |
mojo build <f> |
编译出可执行文件/库/IR | 交付、性能测试、交叉编译 |
mojo repl |
交互式 | 探 API、验证想法 |
mojo debug <f> |
LLDB 调试 | 排查逻辑错误、崩溃 |
mojo precompile <pkg> |
产出 .mojopkg |
库分发、加速大项目编译 |
mojo format <fs> |
格式化 | 提交前、CI |
| `mojo doc <f | dir>` | 产出 docstring JSON |
mojo demangle <name> |
还原符号 | 分析二进制、FFI |
外加两个独立工具:mblack(格式化)、mojo-lsp-server(编辑器)、gpu-query(GPU 信息,需 MAX)。
3.12 编译缓存与性能
Mojo 有编译缓存(.mojo_cache):
bash
mojo --print-cache-location
- 缓存放在项目目录(或按配置),重复编译相同代码会显著加速;
- CI 里建议缓存这个目录;
- 缓存对版本敏感,升级 Mojo 后第一次编译会全量重建。
编译并行度:
bash
mojo build -j 8 app.mojo # 限制到 8 线程(默认用满所有核)
在内存受限的机器上(8 GB),编译大型泛型代码可能 OOM------此时降低 -j 是有效手段。第 26、31 章讨论编译时间的工程化处理。
3.13 一个完整的开发循环(示范)
把本章串起来,一个真实的工作流:
bash
# 1. 建项目
mkdir -p demo/src/demo demo/examples && cd demo
# 2. 写库代码
cat > src/demo/__init__.mojo <<'EOF'
from .stats import mean, stddev
EOF
cat > src/demo/stats.mojo <<'EOF'
from std.math import sqrt
def mean(xs: List[Float64]) raises -> Float64:
"""Return the arithmetic mean of a non-empty list."""
if len(xs) == 0:
raise Error("mean of empty list")
var total = 0.0
for x in xs:
total += x
return total / Float64(len(xs))
def stddev(xs: List[Float64]) raises -> Float64:
"""Return the population standard deviation."""
var m = mean(xs)
var acc = 0.0
for x in xs:
var d = x - m
acc += d * d
return sqrt(acc / Float64(len(xs)))
EOF
# 3. 写调用方
cat > examples/run.mojo <<'EOF'
from demo import mean, stddev
def main() raises:
var xs: List[Float64] = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]
print("mean =", mean(xs))
print("stddev =", stddev(xs))
EOF
# 4. 快速迭代(低优化级别)
mojo run -O0 -I src examples/run.mojo
# 5. 格式化
mblack src/demo/stats.mojo examples/run.mojo
# 6. 发布构建
mojo build -O3 -I src -o run examples/run.mojo
./run
# 7. 看看生成了什么
mojo build -O3 -I src --emit asm examples/run.mojo -o run.s
grep -c vmulpd run.s # 有没有向量化乘法?
第 4 步输出:
output
mean = 5.0
stddev = 2.0
3.14 本章小结
- 顶层只能有声明 和
comptime常量,不能有可执行语句------这是 Mojo 与 Python 的第一个重大差异。 mojo run用于迭代,mojo build用于交付;选项必须写在文件路径之前。--emit能产出asm/llvm/llvm-bitcode/object/shared-lib/air,是理解编译流水线与排查性能的窗口。--fp-mode contract=off关闭 FMA 收缩,做数值精度对齐时必备。mojo repl会回显变量类型,是探 API 的利器(需要libncurses6)。mblack/mojo format统一风格;mojo doc产出 JSON(还没有 HTML)。mojo precompile产.mojopkg,这是 Mojo 的分发单位(因为 ABI 不稳定)。- TString 不支持格式说明符 ,
format()只支持位置索引------对齐/补零要自己写辅助函数。
3.15 动手练习
-
把 3.3 的
wc.mojo扩展成支持-l(只输出行数)、-w(只输出词数)的选项解析。提示:手工遍历std.sys.argv()做前缀匹配即可(Mojo 标准库目前没有 argparse)。 -
用
mojo build --emit asm导出 3.13 的run.s,搜索vmulpd/vaddpd,判断stddev的循环是否被向量化。再用-O0编译一次对比。 -
写一个
@export的函数,mojo build --emit shared-lib出.so,然后用 Pythonctypes调用它:pythonimport ctypes lib = ctypes.CDLL("./libmylib.so") lib.add_one.restype = ctypes.c_int64 print(lib.add_one(41)) -
用
mojo build --print-supported-accelerators列出你机器上能目标化的加速器架构,记下来(第 32 章要用)。 -
故意写三个会编译失败的程序,练习读诊断信息:
- 缺类型标注的函数参数;
- 在非
raises函数里调用raises函数; - 把
List直接赋值给另一个变量(不加.copy()或^)。 把每条错误信息里的error:/note:行抄下来,对照附录 D。
下一章 :第 4 章 词法结构、变量与基本类型 ------ 正式开始语言学习。