forked from dfinity/portal
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
106 changed files
with
5,300 additions
and
2,467 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
# 1: Rust backend canister infrastructure | ||
|
||
## Overview | ||
|
||
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. | ||
|
||
## Rust CDK | ||
|
||
To support Rust development, the IC SDK includes the [Rust canister development kit (Rust CDK)](https://github.com/dfinity/cdk-rs). | ||
|
||
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. | ||
|
||
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: | ||
|
||
1. 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. | ||
|
||
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. | ||
|
||
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. | ||
|
||
For more information on reproducible canister builds, check out [here](../reproducible-builds.md) | ||
|
||
## 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. | ||
|
||
### 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. | ||
|
||
```rust | ||
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: | ||
|
||
```rust | ||
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. | ||
|
||
### 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. | ||
|
||
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. | ||
|
||
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: | ||
|
||
``` | ||
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]); | ||
} | ||
``` | ||
|
||
## Next steps | ||
Now that you've learned about the infrastructure of Rust backend canisters on the Internet Computer, the next step is to learn about [project organization](./2-project-organization.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
# 10: Using periodic timers | ||
|
||
## Overview | ||
This guide demonstrates how to create a small dapp with a periodic task. The task is triggered automatically by the Internet Computer with a specified interval. | ||
|
||
## Prerequisites | ||
|
||
Before getting started, assure you have set up your developer environment according to the instructions in the [developer environment guide](./3-dev-env.md). | ||
|
||
## Create the timer dapp project | ||
|
||
Open a terminal window on your local computer, if you don’t already have one open. | ||
|
||
First, create a new Internet Computer project called `my_timers`: | ||
|
||
```sh | ||
dfx new --type=rust my_timers --no-frontend | ||
``` | ||
|
||
Then, navigate into your project directory by running the command: | ||
|
||
```sh | ||
cd my_timers | ||
``` | ||
|
||
:::info | ||
Note: the following steps assume the terminal is still open and the current directory is `my_timers`. | ||
::: | ||
|
||
## Writing the Cargo.toml file | ||
|
||
First, open the `src/my_timers_backend/Cargo.toml` file in a code editor. Replace the existing contents with the following: | ||
|
||
|
||
```toml | ||
[package] | ||
name = "my_timers_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" | ||
ic-cdk-timers = "0.1" | ||
serde = { version = "1.0", features = ["derive"] } | ||
``` | ||
|
||
Save the file. | ||
|
||
## Update the Cargo.toml dependencies | ||
|
||
Since we made changes to the Cargo.toml file, run the following command to update the project's dependencies: | ||
|
||
``` | ||
cargo update | ||
``` | ||
|
||
## Declaring the canister interface | ||
|
||
Candid is an interface description language (IDL) for interacting with canisters running on the Internet Computer. Candid files provide a language-independent description of canister interfaces. | ||
|
||
To see details about the Candid interface description language syntax, see the [Candid guide](../candid/) or the [Candid crate documentation](https://docs.rs/candid/). | ||
|
||
Start by opening the `src/my_timers_backend/my_timers_backend.did` file in a code editor and replace its content with the following: | ||
|
||
```candid | ||
service : (nat64) -> { | ||
"counter": () -> (nat64) query; | ||
} | ||
``` | ||
|
||
This code establishes the following: | ||
|
||
- `service : (nat64) -> {...}` declares a new service which accepts a single integer argument. | ||
- `"counter": () -> (nat64) query` declares a canister query entry point named `counter`. The `counter` query takes no arguments `()` and returns an integer `(nat64)`. | ||
|
||
Save the file. | ||
|
||
## Implementing the `counter` query | ||
|
||
In the previous step the `counter` query is declared as `"counter": () -> (nat64) query`. This step implements it. | ||
|
||
In the code editor, open the `src/my_timers_backend/src/lib.rs` file and replace its content with the following: | ||
|
||
```rust | ||
use std::sync::atomic::{AtomicU64, Ordering}; | ||
|
||
static COUNTER: AtomicU64 = AtomicU64::new(0); | ||
|
||
#[ic_cdk::query] | ||
fn counter() -> u64 { | ||
COUNTER.load(Ordering::Relaxed) | ||
} | ||
``` | ||
|
||
This code establishes the following: | ||
|
||
- `static COUNTER: AtomicU64 = ...` defines a new global variable called `COUNTER`. | ||
- `#[ic_cdk::query]` marks the following `counter` function as a `query` entry point, so the function will be exported as `canister_query counter`. | ||
- `fn counter() -> u64 {...}` defines the query. Just like in the `.did` definition, it takes no arguments and returns `u64`. | ||
- `COUNTER.load(...)` loads and returns the global `COUNTER` value. | ||
- | ||
|
||
:::info | ||
It is important to note that `Ordering` in this code is a shorter, but equivalent alternative to `thread_local!` wrapping a `Cell<u64>`. | ||
::: | ||
|
||
## Implementing canister initialization | ||
|
||
In the previous step, the service is declared as `service : (nat64) -> {...}`. This step implements the canister initialization with an argument. | ||
|
||
In the code editor, open the `src/my_timers_backend/src/lib.rs` file and append the following: | ||
|
||
```rust | ||
// ... | ||
|
||
#[ic_cdk::init] | ||
fn init(timer_interval_secs: u64) { | ||
let interval = std::time::Duration::from_secs(timer_interval_secs); | ||
ic_cdk::println!("Starting a periodic task with interval {interval:?}"); | ||
ic_cdk_timers::set_timer_interval(interval, || { | ||
COUNTER.fetch_add(1, Ordering::Relaxed); | ||
}); | ||
} | ||
``` | ||
|
||
This code establishes the following: | ||
|
||
- `#[ic_cdk::init]` marks the following `init` function as a canister initialization method, so the function will be run when the canister is installed. | ||
- `fn init(interval: u64) {...}` defines the initialization method. Just like in the `.did` definition, the function takes one argument: timer interval in seconds. | ||
- `ic_cdk::println!(...)` prints the debug log message on the local `dfx` console. | ||
- `ic_cdk_timers::set_timer_interval(...)` creates a new periodic timer with the specified interval and a closure to call. | ||
- `COUNTER.fetch_add(1, ...)` increases the global `COUNTER` every time the periodic task is triggered. | ||
|
||
## Implementing canister upgrade | ||
|
||
:::info | ||
Note: As described in the [periodic tasks and timers](../periodic-tasks) page, the timers library does not handle canister upgrades. It is up to the canister developer to serialize the timers in the `canister_pre_upgrade` and reactivate the timers in the `canister_post_upgrade` method if needed. | ||
::: | ||
|
||
For the sake of simplicity, in this guide the `canister_post_upgrade` method just calls `canister_init` to reinitialize the timer. | ||
|
||
In the code editor, open the `src/my_timers_backend/src/lib.rs` file and append the following: | ||
|
||
```rust | ||
// ... | ||
|
||
#[ic_cdk::post_upgrade] | ||
fn post_upgrade(timer_interval_secs: u64) { | ||
init(timer_interval_secs) | ||
} | ||
``` | ||
|
||
This code establishes the following: | ||
|
||
- `#[ic_cdk::post_upgrade]` marks the following `post_upgrade` function as a canister post-upgrade handler, so the function will be exported as `canister_post_upgrade`. | ||
- `fn post_upgrade(interval: u64) {...}` defines the post-upgrade method. Just like in the `.did` definition, the function takes one argument: timer interval in seconds. | ||
- `init(timer_interval_secs)` for the sake of simplicity, the post-upgrade just calls the `init` function, i.e. does exactly the same as the canister initialization. | ||
|
||
The canister's code is now complete. The finished file should look like this: | ||
|
||
`src/my_timers_backend/src/lib.rs`: | ||
|
||
```rust | ||
use std::sync::atomic::{AtomicU64, Ordering}; | ||
|
||
static COUNTER: AtomicU64 = AtomicU64::new(0); | ||
|
||
#[ic_cdk::query] | ||
fn counter() -> u64 { | ||
COUNTER.load(Ordering::Relaxed) | ||
} | ||
|
||
#[ic_cdk::init] | ||
fn init(timer_interval_secs: u64) { | ||
let interval = std::time::Duration::from_secs(timer_interval_secs); | ||
ic_cdk::println!("Starting a periodic task with interval {interval:?}"); | ||
ic_cdk_timers::set_timer_interval(interval, || { | ||
COUNTER.fetch_add(1, Ordering::Relaxed); | ||
}); | ||
} | ||
|
||
#[ic_cdk::post_upgrade] | ||
fn post_upgrade(timer_interval_secs: u64) { | ||
init(timer_interval_secs) | ||
} | ||
``` | ||
|
||
Save the file. | ||
|
||
## Running the dapp locally | ||
|
||
The libraries are added, the canister interface is described and the code is complete. Time to try it all out! | ||
|
||
Start by assuring that you are still in your project's directory. Then, start the local execution environment with the command: | ||
|
||
```sh | ||
dfx start --clean --background | ||
``` | ||
|
||
Then, compile and deploy `my_timers_backend` canister, setting the interval for the periodic task to 1s: | ||
|
||
```sh | ||
dfx deploy my_timers_backend --argument 1 | ||
``` | ||
|
||
The counter inside the canister starts increasing every second. | ||
|
||
Example output: | ||
|
||
```sh | ||
dfx deploy my_timers_backend --argument 1 | ||
[...] | ||
Deployed canisters. | ||
URLs: | ||
Backend canister via Candid interface: | ||
my_timers_backend: http://127.0.0.1/... | ||
``` | ||
|
||
Then, observe that the counter is actually non-zero: | ||
|
||
```sh | ||
dfx canister call my_timers_backend counter | ||
``` | ||
|
||
Example output: | ||
|
||
```sh | ||
dfx canister call my_timers_backend counter | ||
(8 : nat64) | ||
``` | ||
|
||
## Next steps | ||
For the next step, let's dive into Rust [stable structures](./stable-structures). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# 11: Stable structures | ||
|
||
## Overview | ||
|
||
Stable structures are designed to use stable memory as the backing store, allowing them to grow to gigabytes in size without the need for `pre_upgrade`/`post_upgrade` hooks. | ||
|
||
A [stable structure library](https://github.com/dfinity/stable-structures#readme) exists which aims to simplify managing data structures directly in stable memory and provides example code templates. | ||
|
||
For more information about stable structures, please see the [stable structures library](https://github.com/dfinity/stable-structures) and [Roman's tutorial on stable structures](https://mmapped.blog/posts/14-stable-structures.html). | ||
|
||
## Next steps | ||
|
||
For the next step, let's dive into [storing and searching records](12-searching-records.md). |
Oops, something went wrong.