# 编写客户程序

> 客户程序是一个带入口点和三个内存区域的 no_std Rust 二进制程序。crate 结构、底层运行时、依赖，以及让你能像测试任何 Rust 代码一样测试它的宿主机优先布局。

## 客户程序 crate

在 v1.0.0 中，客户程序（guest）是代码仓库 `guests/` 工作空间中的一个二进制 crate。工作空间提供目标、链接器参数、固定的 profile 和 vendored crate，所以客户程序自己的清单可以保持简短：

```toml title="guests/my-app/Cargo.toml"
[package]
name = "my-app"
version.workspace = true
edition.workspace = true
publish.workspace = true

[dependencies]
guest-sdk.workspace = true
```

```rust title="guests/my-app/src/main.rs"
#![no_std]
#![no_main]

extern crate alloc; // Vec, Box, String, BTreeMap, over the SDK's allocator

use alloc::vec::Vec;

guest_sdk::entry!(main);

fn main() {
    let input = guest_sdk::public_input();
    let mut out = Vec::with_capacity(input.len());
    out.extend(input.iter().rev());
    guest_sdk::commit(&out);
}
```

把 `"my-app"` 加入 `guests/Cargo.toml` 的 `members`，然后在客户程序自己的目录下构建：`cargo build --release --target riscv32imac-unknown-none-elf`。

## 底层运行着什么

客户程序 SDK 就是全部运行时。它小到可以完整列出：

- **启动。** `_start` 位于 `0x0001_0000`，即 `.text` 的第一个字节。它让 `sp` 指向 RAM 顶端，逐字节将 `.bss` 清零，然后调用 `main`。`entry!(f)` 导出的那个 `main` 是对你的函数的包装；你的函数不带参数，返回 `()`。
- **退出。** 从 `main` 返回即为 `exit(0)`。`guest_sdk::exit(code)` 以任意状态结束运行。非零状态表示执行失败，而失败的执行仍然可以被证明：陈述带有这个状态，验证者会读取它。
- **Panic。** panic 处理程序以状态 **101** 退出，不写出任何内容。没有诊断输出流。发生 panic 的客户程序，在 panic 之前提交的内容依然已经发布。
- **堆。** 一个 bump 分配器从紧挨 `.bss` 之上的 `__heap_start` 向上增长。它从不释放内存。参见[堆](#heap)。
- **系统调用。** 客户程序发出的 ecall 只有 `EXIT`，以及 SDK 替你发出的委托调用。输入、证明者提示（advice）和输出都是内存，用普通的加载和存储指令读写。

## 内存布局

客户程序所见的整个 32 位地址空间：

| 范围 | 内容 |
| --- | --- |
| `0x0000_0000 – 0x0000_8000` | 空洞。没有任何东西初始化这一段，所以空指针或野指针会导致致命的 `OutOfBounds`，而不是悄无声息地读出数据 |
| `0x0000_8000 – 0x0000_C000` | 公开输入窗口，16 KiB |
| `0x0000_C000 – 0x0001_0000` | 公开输出（journal）窗口，16 KiB |
| `0x0001_0000 – …` | `.text`（`_start` 在最前），然后是 `.rodata`、`.data` 和 `.bss`，各自按页对齐 |
| `__heap_start` 往上 | 堆，从 `.bss` 末尾向上取整到 16 的位置开始 |
| `0x7F80_0000 – 0x8000_0000` | 栈的 8 MiB 预留区。任何堆块的末端都不得高于 `0x7F80_0000`；栈从 `0x8000_0000` 向下增长 |
| `0x8000_0000 – 2^32` | 证明者提示区域，最多 `2^29` 个字，只有宿主程序（host）实际提供的部分可以寻址 |

代码是静态的。每个 pc 处的指令都取自程序的解码表，从不取自 RAM，所以向 `.text` 写入会改变之后的加载读到的内容，但不会改变执行的指令。

## 堆

分配器向上推进一个指针，`dealloc` 什么也不做。对于一个每个周期都要耗费证明时间的短程序，这是正确的设计，而它也改变了你写 Rust 的方式：

- **让你耗尽内存的是分配总量，而不是峰值。** 一个每轮迭代都构建并丢弃一个 `Vec` 的循环，每次都会消耗新的堆空间。
- **复用缓冲区。** 把分配提到循环之外；用 `clear()` 清空后重新填充，而不是重新分配；用 `with_capacity` 为会增长的集合预设容量，让它们在增长时不必重新分配和复制。
- **上限是退出状态 71。** 一次分配如果末端会高于 `0x7F80_0000`，或高于当前的栈指针，就以状态 71 退出，而不是返回空指针或覆盖栈。

```rust
// Allocates a fresh Vec per record: total heap grows with the record count.
for record in records {
    let fields: Vec<&[u8]> = record.split(|b| *b == b',').collect();
    handle(&fields);
}

// One buffer, reused: total heap is the largest record's field count.
let mut fields: Vec<&[u8]> = Vec::with_capacity(16);
for record in records {
    fields.clear();
    fields.extend(record.split(|b| *b == b','));
    handle(&fields);
}
```

栈有 8 MiB 的预留区，在其中深度递归没有问题。没有任何机制能察觉的情况是：堆已经填满了预留区下方的空间，栈又越过了预留区，这时堆块会在深层调用链之下被改写。让递归保持有界，或者改写成迭代。

## 依赖

任何不依赖 `std`、能为 `riscv32imac-unknown-none-elf` 构建的 crate 都可以用。实践中：

- 关闭默认特性（`default-features = false`），并在 crate 提供 `alloc` 时启用它。
- 引入 `getrandom`、时钟，或带随机种子的 `std::collections::HashMap` 的 crate，在这里没有任何来源可用。这样的调用会得到 `-ENOSYS`，并使这次运行无法被证明。优先使用 `BTreeMap`，或者使用固定、确定性哈希器的哈希表。
- 浮点运算会编译成整数软件例程，因为目标没有 F 或 D 扩展。它正确且确定，但每次运算都要花费许多条指令。整数或定点运算更便宜。
- 哈希和椭圆曲线运算有专用电路。使用 SDK 的函数或 vendored crate，让你的依赖能用上这些电路：[委托](https://apogee.gweb3networks.com/docs/launch/delegations)。

## 先在宿主机上测试

客户程序不打印任何东西，所以调试在宿主机上进行。让这件事变得容易的布局是：把程序写成一个从字节到字节的 `#![no_std]` 库；让 `main.rs` 只负责把字节搬进、搬出各个内存区域；并且只在客户程序目标上依赖 SDK：

```toml title="guests/my-app/Cargo.toml"
[package]
name = "my-app"
version.workspace = true
edition.workspace = true
publish.workspace = true

[target.'cfg(target_arch = "riscv32")'.dependencies]
guest-sdk.workspace = true
```

```rust title="guests/my-app/src/lib.rs"
#![no_std]
extern crate alloc;
use alloc::vec::Vec;

/// The whole application: public input and advice in, journal out.
pub fn run(input: &[u8], advice: &[u8]) -> Result<Vec<u8>, i32> {
    let _ = advice;
    let mut out = Vec::with_capacity(input.len());
    out.extend(input.iter().rev());
    Ok(out)
}
```

```rust title="guests/my-app/src/main.rs"
#![no_std]
#![no_main]

guest_sdk::entry!(main);

fn main() {
    match my_app::run(guest_sdk::public_input(), &[]) {
        Ok(journal) => guest_sdk::commit(&journal),
        Err(code) => guest_sdk::exit(code),
    }
}
```

宿主程序代码随后按路径依赖这个库（就像 `crates/emulator` 依赖 `guests/revm-block` 那样），原生运行 `my_app::run`，再把结果与模拟器对同一输入产生的公开输出相比较（[运行与性能分析](https://apogee.gweb3networks.com/docs/launch/run#emulator)）。你的逻辑在宿主机上可以用单元测试、调试器和 `println!`，而客户程序二进制只是包在已测试代码外面的一层薄壳。

> [!WARNING]
> **两种构建对 `usize` 的理解不同。** 在客户程序上，`usize` 和所有指针都是 32 位；在你的宿主机上是 64 位。`usize` 溢出只在客户程序上触发 panic，`x as usize` 在那里会静默截断，而任何包含长度的东西，其 `size_of` 和 `core::hash` 在两者之间都不相同。不要让 `usize` 出现在你提交、哈希或序列化的任何东西中，在这些边界上使用显式的 `u32` 和 `u64`。

## 汇编与指令集

解码器恰好接受 RV32IMA 的 59 条指令，以及在加载时展开的压缩（C）指令。在这个指令集之内，内联汇编没有问题。任何超出它的东西，例如 CSR 访问、`fence.i`、浮点或 RV64 编码，即使永远不会被执行到，也会让整个程序无法登记：推导时会报告 `Not all opcodes supported: pc=…`。`ebreak`、跳转到一个没有指令的半字，或者未对齐的半字或字访问，都会使运行终止且不产生证明。

原子指令可以解码，也可以证明，只有一处偏差：`sc.w` 总是成功，因为这台机器不保存保留（reservation）状态。[客户程序编程指南](https://apogee.gweb3networks.com/docs/launch/guide#atomics)解释了为什么新的客户程序代码根本不应该使用原子操作。

## 下一步

- [输入、证明者提示与公开输出](https://apogee.gweb3networks.com/docs/launch/io): 三个内存区域，以及证明绑定了什么。

- [委托](https://apogee.gweb3networks.com/docs/launch/delegations): 哈希与曲线运算，成本只是原来的零头。

- [构建与检查](https://apogee.gweb3networks.com/docs/launch/build): 构建 profile、映像、映像报告及其程序身份。
