Introduction to developing canisters in Rust

Introduction to developing canisters in Rust

Rust is a powerful and type-sound modern programming language with an active developer community. Because Rust compiles to WebAssembly, it offers a rich development environment for writing dapps to run on the Internet Computer blockchain.

Rust 是一种功能强大、类型健全的现代编程语言,拥有活跃的开发者社区。 由于 Rust 编译为 WebAssembly,因此它为编写在互联网计算机区块链上运行的 dapp 提供了丰富的开发环境。

Overview

To help pave the way for writing dapps in Rust that can be deployed on the Internet Computer blockchain, you can use the IC SDK. The IC SDK supports the Rust as well as the Motoko programming language. To create a Rust project using the IC SDK, all one needs to do is add the --type=rust flag while creating a new project. For example, here we create a Rust project named hello_world:

为了帮助为用 Rust 编写可部署在互联网计算机区块链上的 dapp 铺平道路,您可以使用 IC SDK。 IC SDK 支持 Rust 和 Motoko 编程语言。 要使用 IC SDK 创建 Rust 项目,只需在创建新项目时添加 --type=rust 标志即可。 例如,这里我们创建一个名为 hello_world 的 Rust 项目:

dfx new --type=rust hello_world

To start a new project see Rust quick start.

Architecture

To support Rust development, the IC SDK includes the Rust canister development kit (Rust CDK).

While using the IC SDK is the typical path for most developers, experienced Rust developers may choose to circumvent IC SDK entirely and use the Rust CDK directly. This documentation assumes one is using the IC SDK to build Rust canisters.

为了支持 Rust 开发,IC SDK 包含 Rust 容器开发套件 (Rust CDK)。


虽然使用 IC SDK 是大多数开发人员的典型路径,但经验丰富的 Rust 开发人员可能会选择完全绕过 IC SDK 并直接使用 Rust CDK。 本文档假设使用 IC SDK 来构建 Rust 容器。

The Rust CDK consists of the following crates:

  • The core of Rust CDK is the ic-cdk crate. It provides the core methods that enable Rust programs to interact with the Internet Computer blockchain system API.

  • The ic-cdk-macros crate defines the procedural macros (e.g. update, query, import) that facilitate building operation endpoints and APIs.

  • Also, the ic-cdk-timers crate provides an API to schedule multiple and periodic tasks.

  • Rust CDK 的核心是 ic-cdk crate。 它提供了使 Rust 程序能够与互联网计算机区块链系统 API 交互的核心方法。
  • ic-cdk-macros crate 定义了有助于构建操作端点和 API 的过程宏(例如更新、查询、导入)。
  • 此外,ic-cdk-timers crate 还提供了一个 API 来安排多个周期性任务。

There are a few examples to get you started building Rust Canisters.

Rust backend canister infrastructure

When developing on the IC, there are currently two primary languages to build backend canisters with; Motoko and Rust. This guide provides an introduction to using Rust to developer backend canisters and covers the basic infrastructure of Rust canisters, as well as design considerations and observability.

在 IC 上进行开发时,目前有两种主要语言可用来构建后端容器: 本子和鲁斯特。 本指南介绍了如何使用 Rust 开发后端容器,并涵盖 Rust 容器的基本基础设施以及设计注意事项和可观察性。

Rust CDK

To support Rust development, the IC SDK includes the Rust canister development kit (Rust CDK).

While using the IC SDK is the typical path for most developers, experienced Rust developers may choose to circumvent IC SDK entirely and use the Rust CDK directly. This documentation assumes one is using the IC SDK to build Rust canisters.

虽然使用 IC SDK 是大多数开发人员的典型路径,但经验丰富的 Rust 开发人员可能会选择完全绕过 IC SDK 并直接使用 Rust CDK。 本文档假设使用 IC SDK 来构建 Rust 容器。

The Rust CDK consists of the following crates:

  • The core of Rust CDK is the ic-cdk crate. It provides the core methods that enable Rust programs to interact with the Internet Computer blockchain system API.
  • Also, the ic-cdk-timers crate provides an API to schedule multiple and periodic tasks.

Canister builds

When building a backend canister, it's important to keep two things in mind:

在构建后端容器时,请务必记住两件事:

Making your build reproducible: if other developers or users are utilizing your canister, they may want to verify that the canister is functioning as they expect it to (especially if your canister deals with transferring their tokens). The IC provides the ability for anyone to inspect the SHA256 hash sum of a canister's WebAssembly module to confirm that the hash of the canister matches the hash of a validated, known good canister, allowing for users to determine if a canister's contents have been edited or changed.

使您的构建具有可重复性:如果其他开发人员或用户正在使用您的容器,他们可能希望验证该容器是否按照他们的预期运行(特别是如果您的容器处理转移其令牌)。 该 IC 使任何人都能够检查容器的 WebAssembly 模块的 SHA256 哈希和,以确认容器的哈希与经过验证的已知良好容器的哈希相匹配,从而允许用户确定容器的内容是否已被编辑或 改变了。

2. Planning for canister upgrades: typically, developers can manage without needing upgrades during the canister's initial development cycle. However, losing the canister's state on each deployment of the canister can be inconvenient. Once a canister has been deployed to the mainnet, the only way for new versions of the canister's code to be shipped is through planned upgrades.

规划容器升级:通常,开发人员可以在容器的初始开发周期内进行管理,而无需升级。 然而,在每次部署罐时丢失罐的状态可能会很不方便。 将容器部署到主网后,交付新版本容器代码的唯一方法是通过计划升级。

Making canister builds reproducible

To create a reproducible canister build, there are two popular workflows: Linux containers like Docker and Nix. Container technologies such as Docker are more popular, provide more resources and may be easier to set up. In comparison, Nix builds tend to be more widely reproducible. Either workflow can be used. Typically, building your canister using a public continuous integration system (CI) can help provide easy to follow instructions for reproducing your final project.

要创建可重现的容器构建,有两种流行的工作流程:Docker 和 Nix 等 Linux 容器。 Docker 等容器技术更流行,提供更多资源并且可能更容易设置。 相比之下,Nix 构建往往具有更广泛的可复制性。 可以使用任一工作流程。 通常,使用公共持续集成系统 (CI) 构建容器可以帮助提供易于遵循的说明来重现最终项目。

It is the canister developer’s responsibility to provide a reproducible way of building a WebAssembly module from the published sources. If your code is still within development, it can help to provide users or other developers with module hashes that correlate to each released version of the project's source code.

容器开发人员有责任提供一种从已发布的源构建 WebAssembly 模块的可重复方法。 如果您的代码仍在开发中,它可以帮助向用户或其他开发人员提供与项目源代码的每个发布版本相关的模块哈希值。

For more information on reproducible canister builds, check out here

Observability

Metrics can be used to gain insight into a wide range of information regarding your canister's production services. This data is important to learn about your canister's statistics and productivity.

指标可用于深入了解有关Canister生产服务的各种信息。 该数据对于了解Canister容器的统计数据和生产力非常重要。

Exposing canister metrics
Approach 1: Expose a query call that returns a data structure containing your canister's metrics.

If this data is not intended to be public, this query can be configured to be rejected based on the caller's principal. This approach provides an response that is structured and easy to parse.

如果此数据不打算公开,则可以根据调用者的主体将此查询配置为拒绝。 这种方法提供了结构化且易于解析的响应。

pub struct MyMetrics {   
pub stable_memory_size: u32,
pub allocated_bytes: u32,
pub my_user_map_size: u64,
pub last_upgraded_ts: u64,
}
#[query]
fn metrics() -> MyMetrics {
check_acl();
MyMetrics {
// ...
}
}
Approach 2: Expose the canister's metrics in a format that your monitoring system can ingest through the canister's HTTP gateway.

For text-based exposition formats, the following example can be used:

fn http_request(req: HttpRequest) -> HttpResponse {   
match path(&req) {
"/metrics" => HttpResponse {
status_code: 200,
body: format!("\
​stable_memory_bytes {}
​allocated_bytes {}
​registered_users_total {}",
stable_memory_bytes, allocated_bytes, num_users),
// ...
}
}
}

Important metric data to watch

  • The size of the canister's stable memory.
  • The size of the canister's internal data structures
  • The sizes of objects allocated within the heap.
  • The date and time the canister was last upgraded.
Globally mutable states

By design, canisters on the IC are structured in a way that forces developers to use a global mutable state. However, Rust's design makes it difficult to global mutable variables. This results in Rust developers needing to choose a method of code organization that takes the IC's design into consideration. This guide will cover a few of those code organization options.

根据设计,IC 上的容器的结构方式迫使开发人员使用全局可变状态。 然而,Rust 的设计使得全局可变变量变得困难。 这导致 Rust 开发人员需要选择一种考虑 IC 设计的代码组织方法。 本指南将介绍其中一些代码组织选项。

Using thread_local! with Cell/RefCell for state variables

Using thread_local! with Cell/RefCell is the safest option to avoid issues with asynchrony and memory corruption.

使用thread_local! 使用 Cell/RefCell 是避免异步和内存损坏问题的最安全选择。

The following is an example of how thread_local! can be used:

thread_local! {
static NEXT_USER_ID: Cell<u64> = Cell::new(0);
static ACTIVE_USERS: RefCell<UserMap> = RefCell::new(UserMap::new());
}
Canister code should be target-independent

It pays off to factor most of the canister code into loosely coupled modules and packages and to test them independently. Most of the code that depends on the System API should go into the main file.

将大部分容器代码分解为松散耦合的模块和包并独立测试它们是值得的。 大多数依赖于系统 API 的代码应该放入主文件中。


It is also possible to create a thin abstraction for the System API and test your code with a fake but faithful implementation. For example, we could use the following trait to abstract the stable memory API:

还可以为系统 API 创建一个简单的抽象,并使用虚假但忠实的实现来测试您的代码。 例如,我们可以使用以下特征来抽象稳定内存 API:

pub trait Memory {
fn size(&self) -> WasmPages;
fn grow(&self, pages: WasmPages) -> WasmPages;
fn read(&self, offset: u32, dst: &mut [u8]);
fn write(&self, offset: u32, src: &[u8]);
}

Project organization

When a new Rust project is created with the command:

dfx new --type rust example

the following project structure is generated:

Cargo.lock
Cargo.toml
dfx.json
package.json
src
├── example_backend
│   ├── Cargo.toml
│   ├── example_backend.did
│   └── src
│   └── lib.rs
└── example_frontend
├── assets
│   ├── favicon.ico
│   ├── logo2.svg
│   ├── main.css
│   └── sample-asset.txt
└── src
├── index.html
└── index.js
webpack.config.js

In this structure, you can see the backend canister, in this case example_backend contains the following components:

src/example_backend:

│   ├── Cargo.toml //
│   ├── example_backend.did // The backend canister's Candid file.
│   └── src
│   └── lib.rs // The file containing your Rust smart contract.

dfx.json

One of the template files included in your project directory is a default dfx.json configuration file. This file contains settings required to build a project for the Internet Computer blockchain much like the Cargo.toml file provides build and package management configuration details for Rust programs.

项目目录中包含的模板文件之一是默认的 dfx.json 配置文件。 该文件包含为互联网计算机区块链构建项目所需的设置,就像 Cargo.toml 文件为 Rust 程序提供构建和包管理配置详细信息一样。

The configuration file should look like this:

{
"canisters": {
"example_backend": {
"candid": "src/example_backend/example_backend.did",
"package": "example_backend",
"type": "rust"
},
"example_frontend": {
"dependencies": [
"example_backend"
],
"frontend": {
"entrypoint": "src/example_frontend/src/index.html"
},
"source": [
"src/example_frontend/assets",
"dist/example_frontend/"
],
"type": "assets"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"version": 1
}

Notice that under the canisters key, you have some default settings for the example_backend canister.

  • "type": "rust" specifies that example_backend is a rust type canister.

  • "candid": "src/example_backend/example_backend.did"" specifies the location of the Candid interface description file to use for the canister.

  • "package": "example_backend" specifies the package name of the Rust crate. It should be the same as in the crate Cargo.toml file.

Cargo.toml

In the root directory, there is a Cargo.toml file.

It defines a Rust workspace by specifying paths to each Rust crate. A Rust type canister is just a Rust crate compiled to WebAssembly. Here we have one member at src/example_backend which is the only Rust canister.

它通过指定每个 Rust 箱的路径来定义 Rust 工作区。 Rust 类型容器只是编译为 WebAssembly 的 Rust 箱。 这里我们在 src/example_backend 有一个成员,这是唯一的 Rust 容器。

[workspace]
members = [
"src/example_backend",
]

src/example_backend/

Now we are in the Rust canister. As any standard Rust crate, it has a Cargo.toml file which configures the details to build the Rust crate.

src/example_backend/Cargo.toml
[package]
name = "example_backend"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
candid = "0.8.2"
ic-cdk = "0.7.0"
serde = { version = "1.0", features = ["derive"] }

Notice the crate-type = ["cdylib"] line which is necessary to compile this Rust program into WebAssembly module.

src/example_backend/src/lib.rs

The default project has a simple greet function that uses the Rust CDK query macro.

#[ic_cdk::query]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}

src/example_backend/example_backend.did

Candid is an interface description language (IDL) for interacting with canisters running on the Internet Computer. Candid files provide a language-independent description of a canister’s interfaces including the names, parameters, and result formats and data types for each function a canister defines.

By adding Candid files to your project, you can ensure that data is properly converted from its definition in Rust to run safely on the Internet Computer blockchain.

To see details about the Candid interface description language syntax, see the Candid Guide or the Candid crate documentation.

service : {
"greet": (text) -> (text) query;
}

This definition specifies that the greet function is a query method which takes text data as input and returns text data.

Declaring global variables

Stable variables vs flexible variables

Stable variables are global variables that the system preserves across upgrades. For example, a user database should probably be stable.

Flexible variables are global variables that the system discards on code upgrade. For example, it is reasonable to make a cache flexible if keeping this cache hot is not critical for your product.

Putting all global variables in one place

It is best practice to store all global variables privately in a single file; the canister main file. This approach is considered the best practice because:

  • Testing your code is easier since the majority of your code won't interact with the global variables directly.
  • It is easier to understand how the global state is being used by the canister.

It is also recommended that you add comments that within your code that specify which variables are stable, such as:

thread_local! {
// static
static USERS: RefCell<Users> = ... ;
// flexible
static LAST_ACTIVE: Cell<UserId> = ...;
}
Canister interfaces

In comparison to Motoko, where the compiler automatically generates the corresponding Candid file, a different approach is recommended to Rust development.

Making the .did file the canister's source of truth

Your Candid file should be the main source of documentation for people who want to interact with your canister, including your colleagues who work on the front end portion. The interface should be stable, easy to find, and well documented, which is not something you can get automatically.

您的 Candid 文件应该是想要与您的canister,容器交互的人员(包括从事前端部分工作的同事)的主要文档来源。 接口应该稳定、易于查找、文档齐全,这不是你可以自动获得的。

The following is an example of a .did file:

type TransferError = variant {
// The debit account didn't have enough funds
// for completing the transaction.
InsufficientFunds : Balance;
// ...
};
type TransferResult =
variant { Ok : BlockHeight; Err : TransferError; };
service {
// Transfer funds between accounts.
transfer : (TransferArgs) -> (TransferResult);
}

For more information on Candid and to see the Rust equivalent of Candid types, please see the Candid reference documentation.

To make sure that your .did file and your implementation are in sync with one another, use the Candid tooling. There are macros in the Rust CDK that allow you to annotate your Canister methods and extract the .did file.

The latest versions of the Candid package have functions to check that one interface is a subtype of another interface, which is a Candid term for “backward compatible”.

Using variant types to indicate error cases

Rust error types tend to make it easy to recover from errors correctly for API consumers, while Candid variants can help clients handle edge case errors more gracefully. Using variant types is also the preferred method of error handling in Motoko.

The following is an example of using variant types to indicate error cases:

type CreateEntityResult = variant {
Ok : record { entity_id : EntityId; };
Err : opt variant {
EntityAlreadyExists;
NoSpaceLeftInThisShard;
}
};
service : {
create_entity : (EntityParams) -> (CreateEntityResult);
}

If a service method returns a result type, it can still reject the call. Therefore, there may not be much benefit from adding error variants like InvalidArgument or Unauthorized, as there is no meaningful way to recover from such errors programmatically. So rejecting malformed, invalid, or unauthorized requests is probably the right thing to do in most situations.

如果服务方法返回结果类型,它仍然可以拒绝调用。 因此,添加 InvalidArgument 或 Unauthorized 等错误变体可能没有多大好处,因为没有有意义的方法以编程方式从此类错误中恢复。 因此,在大多数情况下,拒绝格式错误、无效或未经授权的请求可能是正确的做法。

Introduction to developing canisters in Rust
arkMeta Crypto Network Limited, arkSong 2023年10月9日
標籤
登入 發表評論

Adding and searching simple records
Adding and searching simple records