diff --git a/e2e-tests/src/bin/api.rs b/e2e-tests/src/bin/api.rs new file mode 100644 index 000000000..9abe2f033 --- /dev/null +++ b/e2e-tests/src/bin/api.rs @@ -0,0 +1,158 @@ +use candid::Principal; +use ic_cdk::api::*; + +#[export_name = "canister_update call_msg_arg_data"] +fn call_msg_arg_data() { + assert_eq!(msg_arg_data(), vec![42]); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_msg_caller"] +fn call_msg_caller() { + assert_eq!(msg_caller(), Principal::anonymous()); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_msg_reply"] +fn call_msg_reply() { + msg_reply(vec![42]); +} + +#[export_name = "canister_update call_msg_reject"] +fn call_msg_reject() { + msg_reject("e2e test reject"); +} + +#[export_name = "canister_update call_msg_cycles_available"] +fn call_msg_cycles_available() { + assert_eq!(msg_cycles_available(), 0); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_msg_cycles_accept"] +fn call_msg_cycles_accept() { + // The available cycles are 0, so the actual cycles accepted are 0. + assert_eq!(msg_cycles_accept(1000), 0); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_cycles_burn"] +fn call_cycles_burn() { + assert_eq!(cycles_burn(1000), 1000); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_canister_self"] +fn call_canister_self() { + let self_id = canister_self(); + // The sender sended canister ID + let data = msg_arg_data(); + assert_eq!(self_id.as_slice(), data); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_canister_cycle_balance"] +fn call_canister_cycle_balance() { + assert!(canister_cycle_balance() > 0); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_canister_status"] +fn call_canister_status() { + assert_eq!(canister_status(), CanisterStatusCode::Running); + assert_eq!(canister_status(), 1); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_canister_version"] +fn call_canister_version() { + assert!(canister_version() > 0); + msg_reply(vec![]); +} + +#[export_name = "canister_inspect_message"] +fn inspect_message() { + assert!(msg_method_name().starts_with("call_")); + accept_message(); +} + +#[export_name = "canister_update call_stable"] +fn call_stable() { + assert_eq!(stable_size(), 0); + assert_eq!(stable_grow(1), 0); + let data = vec![42]; + stable_write(0, &data); + let mut read_buf = vec![0]; + stable_read(0, &mut read_buf); + assert_eq!(read_buf, data); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_certified_data_set"] +fn call_certified_data_set() { + certified_data_set(vec![42]); + msg_reply(vec![]); +} + +#[export_name = "canister_query call_data_certificate"] +fn call_data_certificate() { + assert!(data_certificate().is_some()); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_time"] +fn call_time() { + assert!(time() > 0); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_performance_counter"] +fn call_performance_counter() { + let t0 = PerformanceCounterType::InstructionCounter; + assert_eq!(t0, 0); + let ic0 = performance_counter(0); + let ic1 = performance_counter(t0); + let ic2 = instruction_counter(); + assert!(ic0 < ic1); + assert!(ic1 < ic2); + + let t1 = PerformanceCounterType::CallContextInstructionCounter; + assert_eq!(t1, 1); + let ccic0 = performance_counter(1); + let ccic1 = performance_counter(t1); + let ccic2 = call_context_instruction_counter(); + assert!(ccic0 < ccic1); + assert!(ccic1 < ccic2); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_is_controller"] +fn call_is_controller() { + // The canister was created by the anonymous principal. + assert!(is_controller(&Principal::anonymous())); + msg_reply(vec![]); +} + +/// This entry point will be called by both update and query calls. +/// The query call will return 0, and the update call will return 1. +#[export_name = "canister_query call_in_replicated_execution"] +fn call_in_replicated_execution() { + let res = match in_replicated_execution() { + true => 1, + false => 0, + }; + msg_reply(vec![res]); +} + +#[export_name = "canister_update call_debug_print"] +fn call_debug_print() { + debug_print("Hello, world!"); + msg_reply(vec![]); +} + +#[export_name = "canister_update call_trap"] +fn call_trap() { + trap("It's a trap!"); +} + +fn main() {} diff --git a/e2e-tests/src/bin/async.rs b/e2e-tests/src/bin/async.rs index 73873dd71..7c66e3f45 100644 --- a/e2e-tests/src/bin/async.rs +++ b/e2e-tests/src/bin/async.rs @@ -31,7 +31,7 @@ async fn panic_after_async() { let value = *lock; // Do not drop the lock before the await point. - let _: (u64,) = ic_cdk::call(ic_cdk::api::id(), "inc", (value,)) + let _: (u64,) = ic_cdk::call(ic_cdk::api::canister_self(), "inc", (value,)) .await .expect("failed to call self"); ic_cdk::api::trap("Goodbye, cruel world.") @@ -47,7 +47,7 @@ async fn panic_twice() { } async fn async_then_panic() { - let _: (u64,) = ic_cdk::call(ic_cdk::api::id(), "on_notify", ()) + let _: (u64,) = ic_cdk::call(ic_cdk::api::canister_self(), "on_notify", ()) .await .expect("Failed to call self"); panic!(); @@ -66,7 +66,7 @@ fn on_notify() { #[update] fn notify(whom: Principal, method: String) { ic_cdk::notify(whom, method.as_str(), ()).unwrap_or_else(|reject| { - ic_cdk::api::trap(&format!( + ic_cdk::api::trap(format!( "failed to notify (callee={}, method={}): {:?}", whom, method, reject )) @@ -89,8 +89,12 @@ async fn greet_self(greeter: Principal) -> String { #[update] async fn invalid_reply_payload_does_not_trap() -> String { // We're decoding an integer instead of a string, decoding must fail. - let result: Result<(u64,), _> = - ic_cdk::call(ic_cdk::api::id(), "greet", ("World".to_string(),)).await; + let result: Result<(u64,), _> = ic_cdk::call( + ic_cdk::api::canister_self(), + "greet", + ("World".to_string(),), + ) + .await; match result { Ok((_n,)) => ic_cdk::api::trap("expected the decoding to fail"), diff --git a/e2e-tests/src/bin/call.rs b/e2e-tests/src/bin/call.rs index 604ed0c56..8fe0b91a2 100644 --- a/e2e-tests/src/bin/call.rs +++ b/e2e-tests/src/bin/call.rs @@ -1,5 +1,5 @@ use candid::Encode; -use ic_cdk::api::id; +use ic_cdk::api::canister_self; use ic_cdk::call::{Call, ConfigurableCall, DecoderConfig, SendableCall}; use ic_cdk::update; @@ -15,27 +15,30 @@ async fn call_foo() { let n = 0u32; let bytes = Encode!(&n).unwrap(); - let res: u32 = Call::new(id(), "foo").call().await.unwrap(); + let res: u32 = Call::new(canister_self(), "foo").call().await.unwrap(); assert_eq!(res, n); - let res: (u32,) = Call::new(id(), "foo").call_tuple().await.unwrap(); + let res: (u32,) = Call::new(canister_self(), "foo") + .call_tuple() + .await + .unwrap(); assert_eq!(res.0, n); - let res = Call::new(id(), "foo").call_raw().await.unwrap(); + let res = Call::new(canister_self(), "foo").call_raw().await.unwrap(); assert_eq!(res, bytes); - Call::new(id(), "foo").call_oneway().unwrap(); + Call::new(canister_self(), "foo").call_oneway().unwrap(); - let res: (u32,) = Call::new(id(), "foo") + let res: (u32,) = Call::new(canister_self(), "foo") .with_guaranteed_response() .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res: (u32,) = Call::new(id(), "foo") + let res: (u32,) = Call::new(canister_self(), "foo") .change_timeout(5) .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res: (u32,) = Call::new(id(), "foo") + let res: (u32,) = Call::new(canister_self(), "foo") .with_cycles(1000) .call_tuple() .await @@ -43,7 +46,7 @@ async fn call_foo() { assert_eq!(res.0, n); let decoder_config = DecoderConfig::default(); - let res: (u32,) = Call::new(id(), "foo") + let res: (u32,) = Call::new(canister_self(), "foo") .with_decoder_config(decoder_config) .call_tuple() .await @@ -63,37 +66,44 @@ async fn call_echo_with_arg() { let n = 1u32; let bytes = Encode!(&n).unwrap(); // call* - let res: u32 = Call::new(id(), "echo").with_arg(n).call().await.unwrap(); + let res: u32 = Call::new(canister_self(), "echo") + .with_arg(n) + .call() + .await + .unwrap(); assert_eq!(res, n); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_arg(n) .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res = Call::new(id(), "echo") + let res = Call::new(canister_self(), "echo") .with_arg(n) .call_raw() .await .unwrap(); assert_eq!(res, bytes); - Call::new(id(), "echo").with_arg(n).call_oneway().unwrap(); + Call::new(canister_self(), "echo") + .with_arg(n) + .call_oneway() + .unwrap(); // with* - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_arg(n) .with_guaranteed_response() .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_arg(n) .change_timeout(5) .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_arg(n) .with_cycles(1000) .call_tuple() @@ -101,7 +111,7 @@ async fn call_echo_with_arg() { .unwrap(); assert_eq!(res.0, n); let decoder_config = DecoderConfig::default(); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_arg(n) .with_decoder_config(decoder_config) .call_tuple() @@ -116,44 +126,44 @@ async fn call_echo_with_args() { let n = 1u32; let bytes = Encode!(&n).unwrap(); // call* - let res: u32 = Call::new(id(), "echo") + let res: u32 = Call::new(canister_self(), "echo") .with_args((n,)) .call() .await .unwrap(); assert_eq!(res, n); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_args((n,)) .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res = Call::new(id(), "echo") + let res = Call::new(canister_self(), "echo") .with_args((n,)) .call_raw() .await .unwrap(); assert_eq!(res, bytes); - Call::new(id(), "echo") + Call::new(canister_self(), "echo") .with_args((n,)) .call_oneway() .unwrap(); // with* - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_args((n,)) .with_guaranteed_response() .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_args((n,)) .change_timeout(5) .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_args((n,)) .with_cycles(1000) .call_tuple() @@ -161,7 +171,7 @@ async fn call_echo_with_args() { .unwrap(); assert_eq!(res.0, n); let decoder_config = DecoderConfig::default(); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_args((n,)) .with_decoder_config(decoder_config) .call_tuple() @@ -176,44 +186,44 @@ async fn call_echo_with_raw_args() { let n = 1u32; let bytes: Vec = Encode!(&n).unwrap(); // call* - let res: u32 = Call::new(id(), "echo") + let res: u32 = Call::new(canister_self(), "echo") .with_raw_args(&bytes) .call() .await .unwrap(); assert_eq!(res, n); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_raw_args(&bytes) .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res = Call::new(id(), "echo") + let res = Call::new(canister_self(), "echo") .with_raw_args(&bytes) .call_raw() .await .unwrap(); assert_eq!(res, bytes); - Call::new(id(), "echo") + Call::new(canister_self(), "echo") .with_raw_args(&bytes) .call_oneway() .unwrap(); // with* - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_raw_args(&bytes) .with_guaranteed_response() .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_raw_args(&bytes) .change_timeout(5) .call_tuple() .await .unwrap(); assert_eq!(res.0, n); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_raw_args(&bytes) .with_cycles(1000) .call_tuple() @@ -221,7 +231,7 @@ async fn call_echo_with_raw_args() { .unwrap(); assert_eq!(res.0, n); let decoder_config = DecoderConfig::default(); - let res: (u32,) = Call::new(id(), "echo") + let res: (u32,) = Call::new(canister_self(), "echo") .with_raw_args(&bytes) .with_decoder_config(decoder_config) .call_tuple() diff --git a/e2e-tests/src/bin/canister_info.rs b/e2e-tests/src/bin/canister_info.rs index 0ba383e18..0d0568883 100644 --- a/e2e-tests/src/bin/canister_info.rs +++ b/e2e-tests/src/bin/canister_info.rs @@ -58,7 +58,11 @@ async fn canister_lifecycle() -> Principal { .unwrap(); update_settings(UpdateSettingsArgs { settings: CanisterSettings { - controllers: Some(vec![ic_cdk::id(), canister_id, Principal::anonymous()]), + controllers: Some(vec![ + ic_cdk::api::canister_self(), + canister_id, + Principal::anonymous(), + ]), compute_allocation: None, memory_allocation: None, freezing_threshold: None, diff --git a/e2e-tests/src/bin/timers.rs b/e2e-tests/src/bin/timers.rs index 03433aaa8..bc912569a 100644 --- a/e2e-tests/src/bin/timers.rs +++ b/e2e-tests/src/bin/timers.rs @@ -93,8 +93,8 @@ fn add_event(event: &'static str) { } #[update] -fn set_global_timer(timestamp: u64) -> u64 { - ic_cdk::api::set_global_timer(timestamp) +fn global_timer_set(timestamp: u64) -> u64 { + ic_cdk::api::global_timer_set(timestamp) } fn main() {} diff --git a/e2e-tests/tests/api.rs b/e2e-tests/tests/api.rs new file mode 100644 index 000000000..797e0f873 --- /dev/null +++ b/e2e-tests/tests/api.rs @@ -0,0 +1,116 @@ +use candid::Principal; +use ic_cdk_e2e_tests::cargo_build_canister; +use pocket_ic::{ErrorCode, PocketIcBuilder, UserError, WasmResult}; + +#[test] +fn call_api() { + let pic = PocketIcBuilder::new() + .with_application_subnet() + .with_nonmainnet_features(true) + .build(); + let wasm = cargo_build_canister("api"); + let canister_id = pic.create_canister(); + pic.add_cycles(canister_id, 100_000_000_000_000); + pic.install_canister(canister_id, wasm, vec![], None); + let sender = Principal::anonymous(); + let res = pic + .update_call(canister_id, sender, "call_msg_arg_data", vec![42]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .update_call(canister_id, sender, "call_msg_caller", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + // `msg_reject_code` and `msg_reject_msg` can't be tested here. + // They are invoked in the reply/reject callback of inter-canister calls. + // So the `call.rs` test covers them. + let res = pic + .update_call(canister_id, sender, "call_msg_reply", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![42])); + let res = pic + .update_call(canister_id, sender, "call_msg_reject", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reject("e2e test reject".to_string())); + let res = pic + .update_call(canister_id, sender, "call_msg_cycles_available", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + // `msg_cycles_refunded` can't be tested here. + // It can only be called in the reply/reject callback of inter-canister calls. + // TODO: Find a way to test it. + let res = pic + .update_call(canister_id, sender, "call_msg_cycles_accept", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .update_call(canister_id, sender, "call_cycles_burn", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .update_call( + canister_id, + sender, + "call_canister_self", + canister_id.as_slice().to_vec(), + ) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .update_call(canister_id, sender, "call_canister_cycle_balance", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .update_call(canister_id, sender, "call_canister_status", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .update_call(canister_id, sender, "call_canister_version", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + // `msg_method_name` and `accept_message` are invoked in the inspect_message entry point. + // Every calls above/below execute the inspect_message entry point. + // So these two API bindings are tested implicitly. + let res = pic + .update_call(canister_id, sender, "call_stable", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .update_call(canister_id, sender, "call_certified_data_set", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .query_call(canister_id, sender, "call_data_certificate", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .update_call(canister_id, sender, "call_time", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + // `global_timer_set` is tested in `timers.rs`. + let res = pic + .update_call(canister_id, sender, "call_performance_counter", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .update_call(canister_id, sender, "call_is_controller", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let res = pic + .update_call(canister_id, sender, "call_in_replicated_execution", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![1])); + let res = pic + .query_call(canister_id, sender, "call_in_replicated_execution", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![0])); + let res = pic + .update_call(canister_id, sender, "call_debug_print", vec![]) + .unwrap(); + assert_eq!(res, WasmResult::Reply(vec![])); + let UserError { code, description } = pic + .update_call(canister_id, sender, "call_trap", vec![]) + .unwrap_err(); + assert_eq!(code, ErrorCode::CanisterCalledTrap); + assert!(description.contains("It's a trap!")); +} diff --git a/e2e-tests/tests/timers.rs b/e2e-tests/tests/timers.rs index 6581965a1..e2ed40e5d 100644 --- a/e2e-tests/tests/timers.rs +++ b/e2e-tests/tests/timers.rs @@ -141,7 +141,7 @@ fn advance_seconds(pic: &PocketIc, seconds: u32) { } #[test] -fn test_set_global_timers() { +fn test_global_timers_set() { let pic = PocketIc::new(); let wasm = cargo_build_canister("timers"); @@ -174,10 +174,10 @@ fn test_set_global_timers() { &pic, canister_id, RawEffectivePrincipal::None, - "set_global_timer", + "global_timer_set", (t2,), ) - .expect("Failed to call set_global_timer"); + .unwrap(); assert!(previous.abs_diff(t1) < 2); // time error no more than 1 nanosecond // Deactivate the timer @@ -185,9 +185,9 @@ fn test_set_global_timers() { &pic, canister_id, RawEffectivePrincipal::None, - "set_global_timer", + "global_timer_set", (0,), ) - .expect("Failed to call set_global_timer"); + .unwrap(); assert!(previous.abs_diff(t2) < 2); // time error no more than 1 nanosecond } diff --git a/ic-cdk-timers/src/lib.rs b/ic-cdk-timers/src/lib.rs index 5bf523733..777e1df63 100644 --- a/ic-cdk-timers/src/lib.rs +++ b/ic-cdk-timers/src/lib.rs @@ -114,7 +114,7 @@ extern "C" fn global_timer() { ( timer, ic_cdk::call( - ic_cdk::api::id(), + ic_cdk::api::canister_self(), " timer_executor", (task_id.0.as_ffi(),), ) @@ -269,7 +269,7 @@ fn update_ic0_timer() { export_name = "canister_update_ic_cdk_internal.timer_executor" )] extern "C" fn timer_executor() { - if ic_cdk::api::caller() != ic_cdk::api::id() { + if ic_cdk::api::msg_caller() != ic_cdk::api::canister_self() { ic_cdk::trap("This function is internal to ic-cdk and should not be called externally."); } let config = ic_cdk::api::call::ArgDecoderConfig { diff --git a/ic-cdk/src/api.rs b/ic-cdk/src/api.rs new file mode 100644 index 000000000..961452c41 --- /dev/null +++ b/ic-cdk/src/api.rs @@ -0,0 +1,583 @@ +//! System API bindings. +//! +//! This module provides Rust ergonomic bindings to the system APIs. +//! +//! Some APIs require more advanced handling and are organized into separate modules: +//! * For the inter-canister calls API, see the [`call`](mod@crate::call) module. +//! * For the stable memory management API, see the . +//! * The basic bindings are provided in this module including [`stable_size`], [`stable_grow`], [`stable_read`] and [`stable_write`]. +//! * The [`stable`](crate::stable) module provides more advanced functionalities, e.g. support for `std::io` traits. +//! +//! APIs that are only available for `wasm32` are not included. +//! As a result, system APIs with a numeric postfix (indicating the data bit width) are bound to names without the postfix. +//! For example, `ic0::msg_cycles_available128` is bound to [`msg_cycles_available`], while `ic0::msg_cycles_available` has no binding. +//! +//! Functions that provide bindings for a single system API method share the same name as the system API. +//! For example, `ic0::msg_reject_code` is bound to [`msg_reject_code`]. +//! +//! Functions that wrap multiple system API methods are named using the common prefix of the wrapped methods. +//! For example, [`msg_arg_data`] wraps both `ic0::msg_arg_data_size` and `ic0::msg_arg_data_copy`. + +use candid::Principal; +use std::convert::TryFrom; + +pub mod call; +pub mod management_canister; +#[doc(hidden)] +#[deprecated( + since = "0.18.0", + note = "The `api::stable` module has been moved to `stable` (crate root)." +)] +pub mod stable; + +/// Gets the message argument data. +pub fn msg_arg_data() -> Vec { + // SAFETY: ic0.msg_arg_data_size is always safe to call. + let len = unsafe { ic0::msg_arg_data_size() }; + let mut bytes = Vec::with_capacity(len); + // SAFETY: + // `bytes`, being mutable and allocated to `len` bytes, is safe to pass to ic0.msg_arg_data_copy with no offset + // ic0.msg_arg_data_copy writes to all of `bytes[0..len]`, so `set_len` is safe to call with the new len. + unsafe { + ic0::msg_arg_data_copy(bytes.as_mut_ptr() as usize, 0, len); + bytes.set_len(len); + } + bytes +} + +/// Gets the identity of the caller, which may be a canister id or a user id. +/// +/// During canister installation or upgrade, this is the id of the user or canister requesting the installation or upgrade. +/// During a system task (heartbeat or global timer), this is the id of the management canister. +pub fn msg_caller() -> Principal { + // SAFETY: ic0.msg_caller_size is always safe to call. + let len = unsafe { ic0::msg_caller_size() }; + let mut bytes = vec![0u8; len]; + // SAFETY: Because `bytes` is mutable, and allocated to `len` bytes, it is safe to be passed to `ic0.msg_caller_copy` with a 0-offset. + unsafe { + ic0::msg_caller_copy(bytes.as_mut_ptr() as usize, 0, len); + } + // Trust that the system always returns a valid principal. + Principal::try_from(&bytes).unwrap() +} + +/// Returns the reject code, if the current function is invoked as a reject callback. +pub fn msg_reject_code() -> u32 { + // SAFETY: ic0.msg_reject_code is always safe to call. + unsafe { ic0::msg_reject_code() } +} + +/// Gets the reject message. +/// +/// This function can only be called in the reject callback. +/// +/// Traps if there is no reject message (i.e. if reject_code is 0). +pub fn msg_reject_msg() -> String { + // SAFETY: ic0.msg_reject_msg_size is always safe to call. + let len = unsafe { ic0::msg_reject_msg_size() }; + let mut bytes = vec![0u8; len]; + // SAFETY: `bytes`, being mutable and allocated to `len` bytes, is safe to pass to ic0.msg_reject_msg_copy with no offset + unsafe { + ic0::msg_reject_msg_copy(bytes.as_mut_ptr() as usize, 0, len); + } + String::from_utf8_lossy(&bytes).into_owned() +} + +/// Replies to the sender with the data. +pub fn msg_reply>(data: T) { + let buf = data.as_ref(); + if !buf.is_empty() { + // SAFETY: `buf`, being &[u8], is a readable sequence of bytes, and therefore valid to pass to ic0.msg_reply. + unsafe { ic0::msg_reply_data_append(buf.as_ptr() as usize, buf.len()) } + }; + // SAFETY: ic0.msg_reply is always safe to call. + unsafe { ic0::msg_reply() }; +} + +/// Rejects the call with a diagnostic message. +pub fn msg_reject>(message: T) { + let buf = message.as_ref(); + // SAFETY: `buf`, being &str, is a readable sequence of UTF8 bytes and therefore can be passed to ic0.msg_reject. + unsafe { + ic0::msg_reject(buf.as_ptr() as usize, buf.len()); + } +} + +/// Gets the number of cycles transferred by the caller of the current call, still available in this message. +pub fn msg_cycles_available() -> u128 { + let mut recv = 0u128; + // SAFETY: recv is writable and sixteen bytes wide, and therefore is safe to pass to ic0.msg_cycles_available128 + unsafe { + ic0::msg_cycles_available128(&mut recv as *mut u128 as usize); + } + recv +} + +/// Gets the amount of cycles that came back with the response as a refund +/// +/// This function can only be used in a callback handler (reply or reject). +/// The refund has already been added to the canister balance automatically. +pub fn msg_cycles_refunded() -> u128 { + let mut recv = 0u128; + // SAFETY: recv is writable and sixteen bytes wide, and therefore is safe to pass to ic0.msg_cycles_refunded128 + unsafe { + ic0::msg_cycles_refunded128(&mut recv as *mut u128 as usize); + } + recv +} + +/// Moves cycles from the call to the canister balance. +/// +/// The actual amount moved will be returned. +pub fn msg_cycles_accept(max_amount: u128) -> u128 { + let high = (max_amount >> 64) as u64; + let low = (max_amount & u64::MAX as u128) as u64; + let mut recv = 0u128; + // SAFETY: `recv` is writable and sixteen bytes wide, and therefore safe to pass to ic0.msg_cycles_accept128 + unsafe { + ic0::msg_cycles_accept128(high, low, &mut recv as *mut u128 as usize); + } + recv +} + +/// Burns cycles from the canister. +/// +/// Returns the amount of cycles that were actually burned. +pub fn cycles_burn(amount: u128) -> u128 { + let amount_high = (amount >> 64) as u64; + let amount_low = (amount & u64::MAX as u128) as u64; + let mut dst = 0u128; + // SAFETY: `dst` is writable and sixteen bytes wide, and therefore safe to pass to ic0.cycles_burn128 + unsafe { ic0::cycles_burn128(amount_high, amount_low, &mut dst as *mut u128 as usize) } + dst +} + +/// Gets canister's own identity. +pub fn canister_self() -> Principal { + // SAFETY: ic0.canister_self_size is always safe to call. + let len = unsafe { ic0::canister_self_size() }; + let mut bytes = vec![0u8; len]; + // SAFETY: Because `bytes` is mutable, and allocated to `len` bytes, it is safe to be passed to `ic0.canister_self_copy` with a 0-offset. + unsafe { + ic0::canister_self_copy(bytes.as_mut_ptr() as usize, 0, len); + } + // Trust that the system always returns a valid principal. + Principal::try_from(&bytes).unwrap() +} + +/// Gets the current cycle balance of the canister +pub fn canister_cycle_balance() -> u128 { + let mut recv = 0u128; + // SAFETY: recv is writable and the size expected by ic0.canister_cycle_balance128. + unsafe { ic0::canister_cycle_balance128(&mut recv as *mut u128 as usize) } + recv +} + +/// Gets the status of the canister. +/// +/// The status is one of the following: +/// * 1: Running +/// * 2: Stopping +/// * 3: Stopped +pub fn canister_status() -> CanisterStatusCode { + // SAFETY: ic0.canister_status is always safe to call. + unsafe { ic0::canister_status() }.into() +} + +/// The status of a canister. +/// +/// See [Canister status](https://internetcomputer.org/docs/current/references/ic-interface-spec/#system-api-canister-status). +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[repr(u32)] +pub enum CanisterStatusCode { + /// Running. + Running = 1, + /// Stopping. + Stopping = 2, + /// Stopped. + Stopped = 3, + /// A status code that is not recognized by this library. + Unrecognized(u32), +} + +impl From for CanisterStatusCode { + fn from(value: u32) -> Self { + match value { + 1 => Self::Running, + 2 => Self::Stopping, + 3 => Self::Stopped, + _ => Self::Unrecognized(value), + } + } +} + +impl From for u32 { + fn from(value: CanisterStatusCode) -> Self { + match value { + CanisterStatusCode::Running => 1, + CanisterStatusCode::Stopping => 2, + CanisterStatusCode::Stopped => 3, + CanisterStatusCode::Unrecognized(value) => value, + } + } +} + +impl PartialEq for CanisterStatusCode { + fn eq(&self, other: &u32) -> bool { + let self_as_u32: u32 = (*self).into(); + self_as_u32 == *other + } +} + +/// Gets the canister version. +/// +/// See [Canister version](https://internetcomputer.org/docs/current/references/ic-interface-spec/#system-api-canister-version). +pub fn canister_version() -> u64 { + // SAFETY: ic0.canister_version is always safe to call. + unsafe { ic0::canister_version() } +} + +/// Gets the name of the method to be inspected. +/// +/// This function is only available in the `canister_inspect_message` context. +pub fn msg_method_name() -> String { + // SAFETY: ic0.msg_method_name_size is always safe to call. + let len: u32 = unsafe { ic0::msg_method_name_size() as u32 }; + let mut bytes = vec![0u8; len as usize]; + // SAFETY: `bytes` is writable and allocated to `len` bytes, and therefore can be safely passed to ic0.msg_method_name_copy + unsafe { + ic0::msg_method_name_copy(bytes.as_mut_ptr() as usize, 0, len as usize); + } + String::from_utf8_lossy(&bytes).into_owned() +} + +/// Accepts the message in `canister_inspect_message`. +/// +/// This function is only available in the `canister_inspect_message` context. +/// This function traps if invoked twice. +pub fn accept_message() { + // SAFETY: ic0.accept_message is always safe to call. + unsafe { ic0::accept_message() } +} + +/// Gets the current size of the stable memory (in WebAssembly pages). +/// +/// One WebAssembly page is 64KiB. +pub fn stable_size() -> u64 { + // SAFETY: ic0.stable64_size is always safe to call. + unsafe { ic0::stable64_size() } +} + +/// Attempts to grow the stable memory by `new_pages` many pages containing zeroes. +/// +/// One WebAssembly page is 64KiB. +/// +/// If successful, returns the previous size of the memory (in pages). +/// Otherwise, returns `u64::MAX`. +pub fn stable_grow(new_pages: u64) -> u64 { + // SAFETY: ic0.stable64_grow is always safe to call. + unsafe { ic0::stable64_grow(new_pages) } +} + +/// Writes data to the stable memory location specified by an offset. +/// +/// # Warning +/// This will panic if `offset + buf.len()` exceeds the current size of stable memory. +/// Call [`stable_grow`] to request more stable memory if needed. +pub fn stable_write(offset: u64, buf: &[u8]) { + // SAFETY: `buf`, being &[u8], is a readable sequence of bytes, and therefore valid to pass to ic0.stable64_write. + unsafe { + ic0::stable64_write(offset, buf.as_ptr() as u64, buf.len() as u64); + } +} + +/// Reads data from the stable memory location specified by an offset. +/// +/// # Warning +/// This will panic if `offset + buf.len()` exceeds the current size of stable memory. +pub fn stable_read(offset: u64, buf: &mut [u8]) { + // SAFETY: `buf`, being &mut [u8], is a writable sequence of bytes, and therefore valid to pass to ic0.stable64_read. + unsafe { + ic0::stable64_read(buf.as_ptr() as u64, offset, buf.len() as u64); + } +} + +/// Sets the certified data of this canister. +/// +/// Canisters can store up to 32 bytes of data that is certified by +/// the system on a regular basis. One can call [data_certificate] +/// function from a query call to get a certificate authenticating the +/// value set by calling this function. +/// +/// This function can only be called from the following contexts: +/// * "canister_init", "canister_pre_upgrade" and "canister_post_upgrade" +/// hooks. +/// * "canister_update" calls. +/// * reply or reject callbacks. +/// +/// # Panics +/// +/// * This function traps if data.len() > 32. +/// * This function traps if it's called from an illegal context +/// (e.g., from a query call). +pub fn certified_data_set>(data: T) { + let buf = data.as_ref(); + // SAFETY: uf is a slice ref, its pointer and length are valid to pass to ic0.certified_data_set. + unsafe { ic0::certified_data_set(buf.as_ptr() as usize, buf.len()) } +} + +/// When called from a query call, returns the data certificate authenticating +/// certified_data set by this canister. +/// +/// Returns `None` if called not from a query call. +pub fn data_certificate() -> Option> { + // SAFETY: ic0.data_certificate_present is always safe to call. + if unsafe { ic0::data_certificate_present() } == 0 { + return None; + } + // SAFETY: ic0.data_certificate_size is always safe to call. + let n = unsafe { ic0::data_certificate_size() }; + let mut buf = vec![0u8; n]; + // SAFETY: Because `buf` is mutable and allocated to `n` bytes, it is valid to receive from ic0.data_certificate_bytes with no offset + unsafe { + ic0::data_certificate_copy(buf.as_mut_ptr() as usize, 0, n); + } + Some(buf) +} + +/// Gets current timestamp, in nanoseconds since the epoch (1970-01-01) +pub fn time() -> u64 { + // SAFETY: ic0.time is always safe to call. + unsafe { ic0::time() } +} + +/// Sets global timer. +/// +/// The canister can set a global timer to make the system +/// schedule a call to the exported `canister_global_timer` +/// Wasm method after the specified time. +/// The time must be provided as nanoseconds since 1970-01-01. +/// +/// The function returns the previous value of the timer. +/// If no timer is set before invoking the function, then the function returns zero. +/// +/// Passing zero as an argument to the function deactivates the timer and thus +/// prevents the system from scheduling calls to the canister's `canister_global_timer` Wasm method. +pub fn global_timer_set(timestamp: u64) -> u64 { + // SAFETY: ic0.global_timer_set is always safe to call. + unsafe { ic0::global_timer_set(timestamp) } +} + +/// Gets the value of specified performance counter. +/// +/// See [`PerformanceCounterType`] for available counter types. +#[inline] +pub fn performance_counter(counter_type: impl Into) -> u64 { + let counter_type: u32 = counter_type.into().into(); + // SAFETY: ic0.performance_counter is always safe to call. + unsafe { ic0::performance_counter(counter_type) } +} + +/// The type of performance counter. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[repr(u32)] +pub enum PerformanceCounterType { + /// Current execution instruction counter. + /// + /// The number of WebAssembly instructions the canister has executed + /// since the beginning of the current Message execution. + InstructionCounter, + /// Call context instruction counter + /// + /// The number of WebAssembly instructions the canister has executed + /// within the call context of the current Message execution + /// since Call context creation. + /// The counter monotonically increases across all message executions + /// in the call context until the corresponding call context is removed. + CallContextInstructionCounter, + /// A performance counter type that is not recognized by this library. + Unrecognized(u32), +} + +impl From for PerformanceCounterType { + fn from(value: u32) -> Self { + match value { + 0 => Self::InstructionCounter, + 1 => Self::CallContextInstructionCounter, + _ => Self::Unrecognized(value), + } + } +} + +impl From for u32 { + fn from(value: PerformanceCounterType) -> Self { + match value { + PerformanceCounterType::InstructionCounter => 0, + PerformanceCounterType::CallContextInstructionCounter => 1, + PerformanceCounterType::Unrecognized(value) => value, + } + } +} + +impl PartialEq for PerformanceCounterType { + fn eq(&self, other: &u32) -> bool { + let self_as_u32: u32 = (*self).into(); + self_as_u32 == *other + } +} + +/// Returns the number of instructions that the canister executed since the last [entry +/// point](https://internetcomputer.org/docs/current/references/ic-interface-spec/#entry-points). +#[inline] +pub fn instruction_counter() -> u64 { + performance_counter(0) +} + +/// Returns the number of WebAssembly instructions the canister has executed +/// within the call context of the current Message execution since +/// Call context creation. +/// +/// The counter monotonically increases across all message executions +/// in the call context until the corresponding call context is removed. +#[inline] +pub fn call_context_instruction_counter() -> u64 { + performance_counter(1) +} + +/// Determines if a Principal is a controller of the canister. +pub fn is_controller(principal: &Principal) -> bool { + let slice = principal.as_slice(); + // SAFETY: `principal.as_bytes()`, being `&[u8]`, is a readable sequence of bytes and therefore safe to pass to `ic0.is_controller`. + unsafe { ic0::is_controller(slice.as_ptr() as usize, slice.len()) != 0 } +} + +/// Checks if in replicated execution. +/// +/// The canister can check whether it is currently running in replicated or non replicated execution. +pub fn in_replicated_execution() -> bool { + // SAFETY: ic0.in_replicated_execution is always safe to call. + match unsafe { ic0::in_replicated_execution() } { + 0 => false, + 1 => true, + _ => unreachable!(), + } +} + +/// Emits textual trace messages. +/// +/// On the "real" network, these do not do anything. +/// +/// When executing in an environment that supports debugging, this copies out the data +/// and logs, prints or stores it in an environment-appropriate way. +pub fn debug_print>(data: T) { + let buf = data.as_ref(); + // SAFETY: `buf` is a readable sequence of bytes and therefore can be passed to ic0.debug_print. + unsafe { + ic0::debug_print(buf.as_ptr() as usize, buf.len()); + } +} + +/// Traps with the given message. +/// +/// The environment may copy out the data and log, print or store it in an environment-appropriate way, +/// or include it in system-generated reject messages where appropriate. +pub fn trap>(data: T) -> ! { + let buf = data.as_ref(); + // SAFETY: `buf` is a readable sequence of bytes and therefore can be passed to ic0.trap. + unsafe { + ic0::trap(buf.as_ptr() as usize, buf.len()); + } + unreachable!() +} + +// # Deprecated API bindings +// +// The following functions are deprecated and will be removed in the future. +// They are kept here for compatibility with existing code. + +/// Prints the given message. +#[deprecated(since = "0.18.0", note = "Use `debug_print` instead")] +pub fn print>(s: S) { + let s = s.as_ref(); + // SAFETY: `s`, being &str, is a readable sequence of bytes and therefore can be passed to ic0.debug_print. + unsafe { + ic0::debug_print(s.as_ptr() as usize, s.len()); + } +} + +/// Returns the caller of the current call. +#[deprecated(since = "0.18.0", note = "Use `msg_caller` instead")] +pub fn caller() -> Principal { + // SAFETY: ic0.msg_caller_size is always safe to call. + let len = unsafe { ic0::msg_caller_size() }; + let mut bytes = vec![0u8; len]; + // SAFETY: Because `bytes` is mutable, and allocated to `len` bytes, it is safe to be passed to `ic0.msg_caller_copy` with a 0-offset. + unsafe { + ic0::msg_caller_copy(bytes.as_mut_ptr() as usize, 0, len); + } + Principal::try_from(&bytes).unwrap() +} + +/// Returns the canister id as a blob. +#[deprecated(since = "0.18.0", note = "Use `canister_self` instead")] +pub fn id() -> Principal { + // SAFETY: ic0.canister_self_size is always safe to call. + let len = unsafe { ic0::canister_self_size() }; + let mut bytes = vec![0u8; len]; + // SAFETY: Because `bytes` is mutable, and allocated to `len` bytes, it is safe to be passed to `ic0.canister_self_copy` with a 0-offset. + unsafe { + ic0::canister_self_copy(bytes.as_mut_ptr() as usize, 0, len); + } + Principal::try_from(&bytes).unwrap() +} + +/// Gets the amount of funds available in the canister. +#[deprecated(since = "0.18.0", note = "Use `canister_cycle_balance` instead")] +pub fn canister_balance128() -> u128 { + let mut recv = 0u128; + // SAFETY: recv is writable and the size expected by ic0.canister_cycle_balance128. + unsafe { ic0::canister_cycle_balance128(&mut recv as *mut u128 as usize) } + recv +} + +/// Sets the certified data of this canister. +/// +/// Canisters can store up to 32 bytes of data that is certified by +/// the system on a regular basis. One can call [data_certificate] +/// function from a query call to get a certificate authenticating the +/// value set by calling this function. +/// +/// This function can only be called from the following contexts: +/// * "canister_init", "canister_pre_upgrade" and "canister_post_upgrade" +/// hooks. +/// * "canister_update" calls. +/// * reply or reject callbacks. +/// +/// # Panics +/// +/// * This function traps if data.len() > 32. +/// * This function traps if it's called from an illegal context +/// (e.g., from a query call). +#[deprecated(since = "0.18.0", note = "Use `certified_data_set` instead")] +pub fn set_certified_data(data: &[u8]) { + // SAFETY: because data is a slice ref, its pointer and length are valid to pass to ic0.certified_data_set. + unsafe { ic0::certified_data_set(data.as_ptr() as usize, data.len()) } +} + +/// Sets global timer. +/// +/// The canister can set a global timer to make the system +/// schedule a call to the exported canister_global_timer +/// Wasm method after the specified time. +/// The time must be provided as nanoseconds since 1970-01-01. +/// +/// The function returns the previous value of the timer. +/// If no timer is set before invoking the function, then the function returns zero. +/// +/// Passing zero as an argument to the function deactivates the timer and thus +/// prevents the system from scheduling calls to the canister's canister_global_timer Wasm method. +#[deprecated(since = "0.18.0", note = "Use `global_timer_set` instead")] +pub fn set_global_timer(timestamp: u64) -> u64 { + // SAFETY: ic0.global_timer_set is always safe to call. + unsafe { ic0::global_timer_set(timestamp) } +} diff --git a/ic-cdk/src/api/call.rs b/ic-cdk/src/api/call.rs index 1d89f782b..bd8da8768 100644 --- a/ic-cdk/src/api/call.rs +++ b/ic-cdk/src/api/call.rs @@ -1,4 +1,6 @@ //! APIs to make and manage calls in the canister. + +#![allow(deprecated)] use crate::api::trap; use candid::utils::{decode_args_with_config_debug, ArgumentDecoder, ArgumentEncoder}; use candid::{ @@ -758,7 +760,7 @@ pub fn arg_data ArgumentDecoder<'a>>(arg_config: ArgDecoderConfig) -> let config = arg_config.to_candid_config(); let res = decode_args_with_config_debug(&bytes, &config); match res { - Err(e) => trap(&format!("failed to decode call arguments: {:?}", e)), + Err(e) => trap(format!("failed to decode call arguments: {:?}", e)), Ok((r, cost)) => { if arg_config.debug { print_decoding_debug_info("Argument", &cost, None); diff --git a/ic-cdk/src/api/management_canister/http_request/mod.rs b/ic-cdk/src/api/management_canister/http_request/mod.rs index acbb883ab..21c34f1d8 100644 --- a/ic-cdk/src/api/management_canister/http_request/mod.rs +++ b/ic-cdk/src/api/management_canister/http_request/mod.rs @@ -51,7 +51,7 @@ extern "C" fn http_transform() { let key = DefaultKey::from(KeyData::from_ffi(int)); let func = TRANSFORMS_LEGACY.with(|transforms| transforms.borrow_mut().remove(key)); let Some(func) = func else { - crate::trap(&format!("Missing transform function for request {int}")); + crate::trap(format!("Missing transform function for request {int}")); }; let transformed = func(args.response); reply((transformed,)) diff --git a/ic-cdk/src/api/management_canister/mod.rs b/ic-cdk/src/api/management_canister/mod.rs index dd9c83a77..532f09c75 100644 --- a/ic-cdk/src/api/management_canister/mod.rs +++ b/ic-cdk/src/api/management_canister/mod.rs @@ -9,7 +9,7 @@ //! //! [1]: https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-management-canister //! [2]: https://internetcomputer.org/assets/files/ic-a45d11feb0ba0494055083f9d2d21ddf.did - +#![allow(deprecated)] pub mod bitcoin; pub mod ecdsa; pub mod http_request; diff --git a/ic-cdk/src/api/mod.rs b/ic-cdk/src/api/mod.rs deleted file mode 100644 index f54c185ae..000000000 --- a/ic-cdk/src/api/mod.rs +++ /dev/null @@ -1,246 +0,0 @@ -//! System API and low level functions for it. -use candid::Principal; -use std::convert::TryFrom; - -pub mod call; -pub mod management_canister; -pub mod stable; - -/// Prints the given message. -pub fn print>(s: S) { - let s = s.as_ref(); - // SAFETY: `s`, being &str, is a readable sequence of bytes and therefore can be passed to ic0.debug_print. - unsafe { - ic0::debug_print(s.as_ptr() as usize, s.len()); - } -} - -/// Traps with the given message. -pub fn trap(message: &str) -> ! { - // SAFETY: `message`, being &str, is a readable sequence of bytes and therefore can be passed to ic0.trap. - unsafe { - ic0::trap(message.as_ptr() as usize, message.len()); - } - unreachable!() -} - -/// Gets current timestamp, in nanoseconds since the epoch (1970-01-01) -pub fn time() -> u64 { - // SAFETY: ic0.time is always safe to call. - unsafe { ic0::time() } -} - -/// Returns the caller of the current call. -pub fn caller() -> Principal { - // SAFETY: ic0.msg_caller_size is always safe to call. - let len = unsafe { ic0::msg_caller_size() }; - let mut bytes = vec![0u8; len]; - // SAFETY: Because `bytes` is mutable, and allocated to `len` bytes, it is safe to be passed to `ic0.msg_caller_copy` with a 0-offset. - unsafe { - ic0::msg_caller_copy(bytes.as_mut_ptr() as usize, 0, len); - } - Principal::try_from(&bytes).unwrap() -} - -/// Returns the canister id as a blob. -pub fn id() -> Principal { - // SAFETY: ic0.canister_self_size is always safe to call. - let len = unsafe { ic0::canister_self_size() }; - let mut bytes = vec![0u8; len]; - // SAFETY: Because `bytes` is mutable, and allocated to `len` bytes, it is safe to be passed to `ic0.canister_self_copy` with a 0-offset. - unsafe { - ic0::canister_self_copy(bytes.as_mut_ptr() as usize, 0, len); - } - Principal::try_from(&bytes).unwrap() -} - -/// Get the canister ID of the current canister. -pub fn canister_self() -> Principal { - // SAFETY: ic0.canister_self_size is always safe to call. - let len = unsafe { ic0::canister_self_size() }; - let mut bytes = vec![0u8; len]; - // SAFETY: Because `bytes` is mutable, and allocated to `len` bytes, it is safe to be passed to `ic0.canister_self_copy` with a 0-offset. - unsafe { - ic0::canister_self_copy(bytes.as_mut_ptr() as usize, 0, len); - } - Principal::try_from(&bytes).unwrap() -} - -/// Gets the amount of funds available in the canister. -pub fn canister_balance128() -> u128 { - let mut recv = 0u128; - // SAFETY: recv is writable and the size expected by ic0.canister_cycle_balance128. - unsafe { ic0::canister_cycle_balance128(&mut recv as *mut u128 as usize) } - recv -} - -/// Sets the certified data of this canister. -/// -/// Canisters can store up to 32 bytes of data that is certified by -/// the system on a regular basis. One can call [data_certificate] -/// function from a query call to get a certificate authenticating the -/// value set by calling this function. -/// -/// This function can only be called from the following contexts: -/// * "canister_init", "canister_pre_upgrade" and "canister_post_upgrade" -/// hooks. -/// * "canister_update" calls. -/// * reply or reject callbacks. -/// -/// # Panics -/// -/// * This function traps if data.len() > 32. -/// * This function traps if it's called from an illegal context -/// (e.g., from a query call). -pub fn set_certified_data(data: &[u8]) { - // SAFETY: because data is a slice ref, its pointer and length are valid to pass to ic0.certified_data_set. - unsafe { ic0::certified_data_set(data.as_ptr() as usize, data.len()) } -} - -/// When called from a query call, returns the data certificate authenticating -/// certified_data set by this canister. -/// -/// Returns None if called not from a query call. -pub fn data_certificate() -> Option> { - // SAFETY: ic0.data_certificate_present is always safe to call. - if unsafe { ic0::data_certificate_present() } == 0 { - return None; - } - - // SAFETY: ic0.data_certificate_size is always safe to call. - let n = unsafe { ic0::data_certificate_size() }; - let mut buf = vec![0u8; n]; - // SAFETY: Because `buf` is mutable and allocated to `n` bytes, it is valid to receive from ic0.data_certificate_bytes with no offset - unsafe { - ic0::data_certificate_copy(buf.as_mut_ptr() as usize, 0, n); - } - Some(buf) -} - -/// Returns the number of instructions that the canister executed since the last [entry -/// point](https://internetcomputer.org/docs/current/references/ic-interface-spec/#entry-points). -#[inline] -pub fn instruction_counter() -> u64 { - performance_counter(0) -} - -/// Returns the number of WebAssembly instructions the canister has executed -/// within the call context of the current Message execution since -/// Call context creation. -/// -/// The counter monotonically increases across all message executions -/// in the call context until the corresponding call context is removed. -#[inline] -pub fn call_context_instruction_counter() -> u64 { - performance_counter(1) -} - -/// Gets the value of specified performance counter. -/// -/// Supported counter types: -/// * `0` : current execution instruction counter. The number of WebAssembly -/// instructions the canister has executed since the beginning of the -/// current Message execution. -/// * `1` : call context instruction counter. The number of WebAssembly -/// instructions the canister has executed within the call context -/// of the current Message execution since Call context creation. -/// The counter monotonically increases across all message executions -/// in the call context until the corresponding call context is removed. -#[inline] -pub fn performance_counter(counter_type: u32) -> u64 { - // SAFETY: ic0.performance_counter is always safe to call. - unsafe { ic0::performance_counter(counter_type) } -} - -/// Gets the value of canister version. -pub fn canister_version() -> u64 { - // SAFETY: ic0.canister_version is always safe to call. - unsafe { ic0::canister_version() } -} - -/// Determines if a Principal is a controller of the canister. -pub fn is_controller(principal: &Principal) -> bool { - let slice = principal.as_slice(); - // SAFETY: `principal.as_bytes()`, being `&[u8]`, is a readable sequence of bytes and therefore safe to pass to `ic0.is_controller`. - unsafe { ic0::is_controller(slice.as_ptr() as usize, slice.len()) != 0 } -} - -/// Burns cycles from the canister. -/// -/// Returns the amount of cycles that were actually burned. -pub fn cycles_burn(amount: u128) -> u128 { - let amount_high = (amount >> 64) as u64; - let amount_low = (amount & u64::MAX as u128) as u64; - let mut dst = 0u128; - // SAFETY: `dst` is writable and sixteen bytes wide, and therefore safe to pass to ic0.cycles_burn128 - unsafe { ic0::cycles_burn128(amount_high, amount_low, &mut dst as *mut u128 as usize) } - dst -} - -/// Sets global timer. -/// -/// The canister can set a global timer to make the system -/// schedule a call to the exported canister_global_timer -/// Wasm method after the specified time. -/// The time must be provided as nanoseconds since 1970-01-01. -/// -/// The function returns the previous value of the timer. -/// If no timer is set before invoking the function, then the function returns zero. -/// -/// Passing zero as an argument to the function deactivates the timer and thus -/// prevents the system from scheduling calls to the canister's canister_global_timer Wasm method. -pub fn set_global_timer(timestamp: u64) -> u64 { - // SAFETY: ic0.global_timer_set is always safe to call. - unsafe { ic0::global_timer_set(timestamp) } -} - -/// Checks if in replicated execution. -/// -/// The canister can check whether it is currently running in replicated or non replicated execution. -pub fn in_replicated_execution() -> bool { - // SAFETY: ic0.in_replicated_execution is always safe to call. - match unsafe { ic0::in_replicated_execution() } { - 0 => false, - 1 => true, - _ => unreachable!(), - } -} - -/// Returns the argument data as bytes. -pub fn msg_arg_data() -> Vec { - // SAFETY: ic0.msg_arg_data_size is always safe to call. - let len = unsafe { ic0::msg_arg_data_size() }; - let mut bytes = Vec::with_capacity(len); - // SAFETY: - // `bytes`, being mutable and allocated to `len` bytes, is safe to pass to ic0.msg_arg_data_copy with no offset - // ic0.msg_arg_data_copy writes to all of `bytes[0..len]`, so `set_len` is safe to call with the new len. - unsafe { - ic0::msg_arg_data_copy(bytes.as_mut_ptr() as usize, 0, len); - bytes.set_len(len); - } - bytes -} - -/// Gets the len of the raw-argument-data-bytes. -pub fn msg_arg_data_size() -> usize { - // SAFETY: ic0.msg_arg_data_size is always safe to call. - unsafe { ic0::msg_arg_data_size() } -} - -/// Returns the rejection code for the call. -pub fn msg_reject_code() -> u32 { - // SAFETY: ic0.msg_reject_code is always safe to call. - unsafe { ic0::msg_reject_code() } -} - -/// Returns the rejection message. -pub fn msg_reject_msg() -> String { - // SAFETY: ic0.msg_reject_msg_size is always safe to call. - let len = unsafe { ic0::msg_reject_msg_size() }; - let mut bytes = vec![0u8; len]; - // SAFETY: `bytes`, being mutable and allocated to `len` bytes, is safe to pass to ic0.msg_reject_msg_copy with no offset - unsafe { - ic0::msg_reject_msg_copy(bytes.as_mut_ptr() as usize, 0, len); - } - String::from_utf8_lossy(&bytes).into_owned() -} diff --git a/ic-cdk/src/api/stable/mod.rs b/ic-cdk/src/api/stable/mod.rs index 5cac56884..fe5101da3 100644 --- a/ic-cdk/src/api/stable/mod.rs +++ b/ic-cdk/src/api/stable/mod.rs @@ -3,8 +3,6 @@ //! You can check the [Internet Computer Specification](https://internetcomputer.org/docs/current/references/ic-interface-spec/#system-api-stable-memory) //! for a in-depth explanation of stable memory. mod canister; -#[cfg(test)] -mod tests; pub use canister::CanisterStableMemory; use std::{error, fmt, io}; @@ -37,11 +35,6 @@ pub trait StableMemory { fn stable_read(&self, offset: u64, buf: &mut [u8]); } -/// Gets current size of the stable memory (in WASM pages). -pub fn stable_size() -> u64 { - CANISTER_STABLE_MEMORY.stable_size() -} - /// A possible error value when dealing with stable memory. #[derive(Debug)] pub enum StableMemoryError { @@ -62,6 +55,11 @@ impl fmt::Display for StableMemoryError { impl error::Error for StableMemoryError {} +/// Gets current size of the stable memory (in WASM pages). +pub fn stable_size() -> u64 { + CANISTER_STABLE_MEMORY.stable_size() +} + /// Attempts to grow the stable memory by `new_pages` (added pages). /// /// Returns an error if it wasn't possible. Otherwise, returns the previous @@ -133,11 +131,6 @@ impl Default for StableIO { } } -// Helper macro to implement StableIO for both 32-bit and 64-bit. -// -// We use a macro here since capturing all the traits required to add and manipulate memory -// addresses with generics becomes cumbersome. - impl StableIO { /// Creates a new `StableIO` which writes to the selected memory pub fn with_memory(memory: M, offset: u64) -> Self { diff --git a/ic-cdk/src/api/stable/tests.rs b/ic-cdk/src/api/stable/tests.rs deleted file mode 100644 index aaf592849..000000000 --- a/ic-cdk/src/api/stable/tests.rs +++ /dev/null @@ -1,265 +0,0 @@ -use super::*; -use std::rc::Rc; -use std::sync::Mutex; - -#[derive(Default)] -pub struct TestStableMemory { - memory: Rc>>, -} - -impl TestStableMemory { - pub fn new(memory: Rc>>) -> TestStableMemory { - let bytes_len = memory.lock().unwrap().len(); - if bytes_len > 0 { - let pages_required = pages_required(bytes_len); - let bytes_required = pages_required * WASM_PAGE_SIZE_IN_BYTES; - memory - .lock() - .unwrap() - .resize(bytes_required.try_into().unwrap(), 0); - } - - TestStableMemory { memory } - } -} - -impl StableMemory for TestStableMemory { - fn stable_size(&self) -> u64 { - let bytes_len = self.memory.lock().unwrap().len(); - pages_required(bytes_len) - } - - fn stable_grow(&self, new_pages: u64) -> Result { - let new_bytes = new_pages * WASM_PAGE_SIZE_IN_BYTES; - - let mut vec = self.memory.lock().unwrap(); - let previous_len = vec.len() as u64; - let new_len = vec.len() as u64 + new_bytes; - vec.resize(new_len.try_into().unwrap(), 0); - Ok(previous_len / WASM_PAGE_SIZE_IN_BYTES) - } - - fn stable_write(&self, offset: u64, buf: &[u8]) { - let offset = offset as usize; - - let mut vec = self.memory.lock().unwrap(); - if offset + buf.len() > vec.len() { - panic!("stable memory out of bounds"); - } - vec[offset..(offset + buf.len())].clone_from_slice(buf); - } - - fn stable_read(&self, offset: u64, buf: &mut [u8]) { - let offset = offset as usize; - - let vec = self.memory.lock().unwrap(); - let count_to_copy = buf.len(); - - buf[..count_to_copy].copy_from_slice(&vec[offset..offset + count_to_copy]); - } -} - -fn pages_required(bytes_len: usize) -> u64 { - let page_size = WASM_PAGE_SIZE_IN_BYTES; - (bytes_len as u64 + page_size - 1) / page_size -} - -mod stable_writer_tests { - use super::*; - use rstest::rstest; - use std::io::{Seek, Write}; - - #[rstest] - #[case(None)] - #[case(Some(1))] - #[case(Some(10))] - #[case(Some(100))] - #[case(Some(1000))] - fn write_single_slice(#[case] buffer_size: Option) { - let memory = Rc::new(Mutex::new(Vec::new())); - let mut writer = build_writer(TestStableMemory::new(memory.clone()), buffer_size); - - let bytes = vec![1; 100]; - - writer.write_all(&bytes).unwrap(); - writer.flush().unwrap(); - - let result = &*memory.lock().unwrap(); - - assert_eq!(bytes, result[..bytes.len()]); - } - - #[rstest] - #[case(None)] - #[case(Some(1))] - #[case(Some(10))] - #[case(Some(100))] - #[case(Some(1000))] - fn write_many_slices(#[case] buffer_size: Option) { - let memory = Rc::new(Mutex::new(Vec::new())); - let mut writer = build_writer(TestStableMemory::new(memory.clone()), buffer_size); - - for i in 1..100 { - let bytes = vec![i as u8; i]; - writer.write_all(&bytes).unwrap(); - } - writer.flush().unwrap(); - - let result = &*memory.lock().unwrap(); - - let mut offset = 0; - for i in 1..100 { - let bytes = &result[offset..offset + i]; - assert_eq!(bytes, vec![i as u8; i]); - offset += i; - } - } - - #[rstest] - #[case(None)] - #[case(Some(1))] - #[case(Some(10))] - #[case(Some(100))] - #[case(Some(1000))] - fn ensure_only_requests_min_number_of_pages_required(#[case] buffer_size: Option) { - let memory = Rc::new(Mutex::new(Vec::new())); - let mut writer = build_writer(TestStableMemory::new(memory.clone()), buffer_size); - - let mut total_bytes = 0; - for i in 1..10000 { - let bytes = vec![i as u8; i]; - writer.write_all(&bytes).unwrap(); - total_bytes += i; - } - writer.flush().unwrap(); - - let capacity_pages = TestStableMemory::new(memory).stable_size(); - let min_pages_required = - (total_bytes as u64 + WASM_PAGE_SIZE_IN_BYTES - 1) / WASM_PAGE_SIZE_IN_BYTES; - - assert_eq!(capacity_pages, min_pages_required); - } - - #[test] - fn check_offset() { - const WRITE_SIZE: usize = 1025; - - let memory = Rc::new(Mutex::new(Vec::new())); - let mut writer = StableWriter::with_memory(TestStableMemory::new(memory.clone()), 0); - assert_eq!(writer.offset(), 0); - assert_eq!(writer.write(&vec![0; WRITE_SIZE]).unwrap(), WRITE_SIZE); - assert_eq!(writer.offset(), WRITE_SIZE as u64); - - let mut writer = BufferedStableWriter::with_writer( - WRITE_SIZE - 1, - StableWriter::with_memory(TestStableMemory::new(memory), 0), - ); - assert_eq!(writer.offset(), 0); - assert_eq!(writer.write(&vec![0; WRITE_SIZE]).unwrap(), WRITE_SIZE); - assert_eq!(writer.offset(), WRITE_SIZE as u64); - } - - #[test] - fn test_seek() { - let memory = Rc::new(Mutex::new(Vec::new())); - let mut writer = StableWriter::with_memory(TestStableMemory::new(memory.clone()), 0); - writer - .seek(std::io::SeekFrom::Start(WASM_PAGE_SIZE_IN_BYTES)) - .unwrap(); - assert_eq!(writer.stream_position().unwrap(), WASM_PAGE_SIZE_IN_BYTES); - assert_eq!(writer.write(&[1_u8]).unwrap(), 1); - assert_eq!( - writer.seek(std::io::SeekFrom::End(0)).unwrap(), - WASM_PAGE_SIZE_IN_BYTES * 2 - ); - let capacity_pages = TestStableMemory::new(memory).stable_size(); - assert_eq!(capacity_pages, 2); - } - - fn build_writer(memory: TestStableMemory, buffer_size: Option) -> Box { - let writer = StableWriter::with_memory(memory, 0); - if let Some(buffer_size) = buffer_size { - Box::new(BufferedStableWriter::with_writer(buffer_size, writer)) - } else { - Box::new(writer) - } - } -} - -mod stable_reader_tests { - use super::*; - use rstest::rstest; - use std::io::{Read, Seek}; - - #[rstest] - #[case(None)] - #[case(Some(1))] - #[case(Some(10))] - #[case(Some(100))] - #[case(Some(1000))] - fn reads_all_bytes(#[case] buffer_size: Option) { - let input = vec![1; 10_000]; - let memory = Rc::new(Mutex::new(input.clone())); - let mut reader = build_reader(TestStableMemory::new(memory), buffer_size); - - let mut output = Vec::new(); - reader.read_to_end(&mut output).unwrap(); - - assert_eq!(input, output[..input.len()]); - } - - #[test] - fn check_offset() { - const READ_SIZE: usize = 1025; - - let memory = Rc::new(Mutex::new(vec![1; READ_SIZE])); - let mut reader = StableReader::with_memory(TestStableMemory::new(memory.clone()), 0); - assert_eq!(reader.offset(), 0); - let mut bytes = vec![0; READ_SIZE]; - assert_eq!(reader.read(&mut bytes).unwrap(), READ_SIZE); - assert_eq!(reader.offset(), READ_SIZE as u64); - - let mut reader = BufferedStableReader::with_reader( - READ_SIZE - 1, - StableReader::with_memory(TestStableMemory::new(memory), 0), - ); - assert_eq!(reader.offset(), 0); - let mut bytes = vec![0; READ_SIZE]; - assert_eq!(reader.read(&mut bytes).unwrap(), READ_SIZE); - assert_eq!(reader.offset(), READ_SIZE as u64); - } - - #[test] - fn test_seek() { - const SIZE: usize = 1025; - let memory = Rc::new(Mutex::new((0..SIZE).map(|v| v as u8).collect::>())); - let mut reader = StableReader::with_memory(TestStableMemory::new(memory), 0); - let mut bytes = vec![0_u8; 1]; - - const OFFSET: usize = 200; - reader - .seek(std::io::SeekFrom::Start(OFFSET as u64)) - .unwrap(); - assert_eq!(reader.stream_position().unwrap() as usize, OFFSET); - assert_eq!(reader.read(&mut bytes).unwrap(), 1); - assert_eq!(&bytes, &[OFFSET as u8]); - assert_eq!( - reader.seek(std::io::SeekFrom::End(0)).unwrap(), - WASM_PAGE_SIZE_IN_BYTES - ); - reader - .seek(std::io::SeekFrom::Start(WASM_PAGE_SIZE_IN_BYTES * 2)) - .unwrap(); - // out of bounds so should fail - assert!(reader.read(&mut bytes).is_err()); - } - - fn build_reader(memory: TestStableMemory, buffer_size: Option) -> Box { - let reader = StableReader::with_memory(memory, 0); - if let Some(buffer_size) = buffer_size { - Box::new(BufferedStableReader::with_reader(buffer_size, reader)) - } else { - Box::new(reader) - } - } -} diff --git a/ic-cdk/src/call.rs b/ic-cdk/src/call.rs index b845a7d25..696e5fcd1 100644 --- a/ic-cdk/src/call.rs +++ b/ic-cdk/src/call.rs @@ -747,15 +747,15 @@ fn print_decoding_debug_info( cost: &candid::de::DecoderConfig, pre_cycles: Option, ) { - use crate::api::{performance_counter, print}; + use crate::api::{debug_print, performance_counter}; let pre_cycles = pre_cycles.unwrap_or(0); let instrs = performance_counter(0) - pre_cycles; - print(format!("[Debug] {title} decoding instructions: {instrs}")); + debug_print(format!("[Debug] {title} decoding instructions: {instrs}")); if let Some(n) = cost.decoding_quota { - print(format!("[Debug] {title} decoding cost: {n}")); + debug_print(format!("[Debug] {title} decoding cost: {n}")); } if let Some(n) = cost.skipping_quota { - print(format!("[Debug] {title} skipping cost: {n}")); + debug_print(format!("[Debug] {title} skipping cost: {n}")); } } diff --git a/ic-cdk/src/lib.rs b/ic-cdk/src/lib.rs index beeb8dfd7..03bbf3e4c 100644 --- a/ic-cdk/src/lib.rs +++ b/ic-cdk/src/lib.rs @@ -20,6 +20,7 @@ mod futures; mod macros; pub mod management_canister; mod printer; +pub mod stable; pub mod storage; use std::sync::atomic::{AtomicBool, Ordering}; @@ -29,7 +30,11 @@ pub use api::call::call; #[doc(inline)] pub use api::call::notify; #[doc(inline)] -pub use api::{caller, id, print, trap}; +pub use api::trap; + +#[doc(inline)] +#[allow(deprecated)] +pub use api::{caller, id, print}; #[doc(inline)] pub use macros::*; @@ -62,8 +67,8 @@ pub fn spawn>(future: F) { #[cfg(target_arch = "wasm32")] #[macro_export] macro_rules! println { - ($fmt:expr) => ($crate::print(format!($fmt))); - ($fmt:expr, $($arg:tt)*) => ($crate::print(format!($fmt, $($arg)*))); + ($fmt:expr) => ($crate::api::debug_print(format!($fmt))); + ($fmt:expr, $($arg:tt)*) => ($crate::api::debug_print(format!($fmt, $($arg)*))); } /// Format and then print the formatted message @@ -78,8 +83,8 @@ macro_rules! println { #[cfg(target_arch = "wasm32")] #[macro_export] macro_rules! eprintln { - ($fmt:expr) => ($crate::print(format!($fmt))); - ($fmt:expr, $($arg:tt)*) => ($crate::print(format!($fmt, $($arg)*))); + ($fmt:expr) => ($crate::api::debug_print(format!($fmt))); + ($fmt:expr, $($arg:tt)*) => ($crate::api::debug_print(format!($fmt, $($arg)*))); } /// Format and then print the formatted message diff --git a/ic-cdk/src/management_canister.rs b/ic-cdk/src/management_canister.rs index 0fe50662b..64f086123 100644 --- a/ic-cdk/src/management_canister.rs +++ b/ic-cdk/src/management_canister.rs @@ -1107,9 +1107,9 @@ mod transform_closure { extern "C" fn http_transform() { use crate::api::{ call::{arg_data, reply, ArgDecoderConfig}, - caller, + msg_caller, }; - if caller() != Principal::management_canister() { + if msg_caller() != Principal::management_canister() { crate::trap("This function is internal to ic-cdk and should not be called externally."); } crate::setup(); @@ -1118,7 +1118,7 @@ mod transform_closure { let key = DefaultKey::from(KeyData::from_ffi(int)); let func = TRANSFORMS.with(|transforms| transforms.borrow_mut().remove(key)); let Some(func) = func else { - crate::trap(&format!("Missing transform function for request {int}")); + crate::trap(format!("Missing transform function for request {int}")); }; let transformed = func(args.response); reply((transformed,)) diff --git a/ic-cdk/src/prelude.rs b/ic-cdk/src/prelude.rs index cb8972db7..d697f1780 100644 --- a/ic-cdk/src/prelude.rs +++ b/ic-cdk/src/prelude.rs @@ -1,5 +1,5 @@ //! The prelude module contains the most commonly used types and traits. -pub use crate::api::{caller, id, print, trap}; +pub use crate::api::{canister_self, debug_print, msg_caller, trap}; pub use crate::call::{Call, CallResult, ConfigurableCall, RejectCode, SendableCall}; pub use crate::macros::{ export_candid, heartbeat, init, inspect_message, post_upgrade, pre_upgrade, query, update, diff --git a/ic-cdk/src/printer.rs b/ic-cdk/src/printer.rs index cfc864e6c..f22fd1707 100644 --- a/ic-cdk/src/printer.rs +++ b/ic-cdk/src/printer.rs @@ -17,7 +17,7 @@ pub fn set_panic_hook() { }; let err_info = format!("Panicked at '{}', {}:{}:{}", msg, file, line, col); - api::print(&err_info); + api::debug_print(&err_info); api::trap(&err_info); })); } diff --git a/ic-cdk/src/stable.rs b/ic-cdk/src/stable.rs new file mode 100644 index 000000000..2c1e84e8c --- /dev/null +++ b/ic-cdk/src/stable.rs @@ -0,0 +1,775 @@ +//! APIs to manage stable memory. +//! +//! You can check the [Internet Computer Specification](https://internetcomputer.org/docs/current/references/ic-interface-spec/#system-api-stable-memory) +//! for a in-depth explanation of stable memory. +// mod canister; +// #[cfg(test)] +// mod tests; + +use std::{error, fmt, io}; + +/// WASM page size in bytes. +pub const WASM_PAGE_SIZE_IN_BYTES: u64 = 64 * 1024; // 64KB + +static CANISTER_STABLE_MEMORY: CanisterStableMemory = CanisterStableMemory {}; + +/// A trait defining the stable memory API which each canister running on the IC can make use of +pub trait StableMemory { + /// Gets current size of the stable memory (in WASM pages). + fn stable_size(&self) -> u64; + + /// Attempts to grow the stable memory by `new_pages` (added pages). + /// + /// Returns an error if it wasn't possible. Otherwise, returns the previous + /// size that was reserved. + /// + /// *Note*: Pages are 64KiB in WASM. + fn stable_grow(&self, new_pages: u64) -> Result; + + /// Writes data to the stable memory location specified by an offset. + /// + /// Warning - this will panic if `offset + buf.len()` exceeds the current size of stable memory. + /// Use `stable_grow` to request more stable memory if needed. + fn stable_write(&self, offset: u64, buf: &[u8]); + + /// Reads data from the stable memory location specified by an offset. + fn stable_read(&self, offset: u64, buf: &mut [u8]); +} + +/// A possible error value when dealing with stable memory. +#[derive(Debug)] +pub enum StableMemoryError { + /// No more stable memory could be allocated. + OutOfMemory, + /// Attempted to read more stable memory than had been allocated. + OutOfBounds, +} + +impl fmt::Display for StableMemoryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::OutOfMemory => f.write_str("Out of memory"), + Self::OutOfBounds => f.write_str("Read exceeds allocated memory"), + } + } +} + +impl error::Error for StableMemoryError {} + +/// A standard implementation of [`StableMemory`]. +/// +/// Useful for creating [`StableWriter`] and [`StableReader`]. +#[derive(Default, Debug, Copy, Clone)] +pub struct CanisterStableMemory {} + +impl StableMemory for CanisterStableMemory { + fn stable_size(&self) -> u64 { + // SAFETY: ic0.stable64_size is always safe to call. + unsafe { ic0::stable64_size() } + } + + fn stable_grow(&self, new_pages: u64) -> Result { + // SAFETY: ic0.stable64_grow is always safe to call. + unsafe { + match ic0::stable64_grow(new_pages) { + u64::MAX => Err(StableMemoryError::OutOfMemory), + x => Ok(x), + } + } + } + + fn stable_write(&self, offset: u64, buf: &[u8]) { + // SAFETY: `buf`, being &[u8], is a readable sequence of bytes, and therefore valid to pass to ic0.stable64_write. + unsafe { + ic0::stable64_write(offset, buf.as_ptr() as u64, buf.len() as u64); + } + } + + fn stable_read(&self, offset: u64, buf: &mut [u8]) { + // SAFETY: `buf`, being &mut [u8], is a writable sequence of bytes, and therefore valid to pass to ic0.stable64_read. + unsafe { + ic0::stable64_read(buf.as_ptr() as u64, offset, buf.len() as u64); + } + } +} + +/// Gets current size of the stable memory (in WASM pages). +pub fn stable_size() -> u64 { + CANISTER_STABLE_MEMORY.stable_size() +} + +/// Attempts to grow the stable memory by `new_pages` (added pages). +/// +/// Returns an error if it wasn't possible. Otherwise, returns the previous +/// size that was reserved. +/// +/// *Note*: Pages are 64KiB in WASM. +pub fn stable_grow(new_pages: u64) -> Result { + CANISTER_STABLE_MEMORY.stable_grow(new_pages) +} + +/// Writes data to the stable memory location specified by an offset. +/// +/// Warning - this will panic if `offset + buf.len()` exceeds the current size of stable memory. +/// Use `stable_grow` to request more stable memory if needed. +pub fn stable_write(offset: u64, buf: &[u8]) { + CANISTER_STABLE_MEMORY.stable_write(offset, buf) +} + +/// Reads data from the stable memory location specified by an offset. +pub fn stable_read(offset: u64, buf: &mut [u8]) { + CANISTER_STABLE_MEMORY.stable_read(offset, buf) +} + +/// Returns a copy of the stable memory. +/// +/// This will map the whole memory (even if not all of it has been written to). +/// +/// # Panics +/// +/// When the bytes of the stable memory cannot fit into a `Vec` which constrained by the usize. +pub fn stable_bytes() -> Vec { + let size = (stable_size() << 16) + .try_into() + .expect("overflow: stable memory too large to read in one go"); + let mut vec = Vec::with_capacity(size); + // SAFETY: + // `vec`, being mutable and allocated to `size` bytes, is safe to pass to ic0.stable_read with no offset. + // ic0.stable_read writes to all of `vec[0..size]`, so `set_len` is safe to call with the new size. + unsafe { + ic0::stable64_read(vec.as_ptr() as u64, 0, size as u64); + vec.set_len(size); + } + vec +} + +/// Performs generic IO (read, write, and seek) on stable memory. +/// +/// Warning: When using write functionality, this will overwrite any existing +/// data in stable memory as it writes, so ensure you set the `offset` value +/// accordingly if you wish to preserve existing data. +/// +/// Will attempt to grow the memory as it writes, +/// and keep offsets and total capacity. +#[derive(Debug)] +pub struct StableIO { + /// The offset of the next write. + offset: u64, + + /// The capacity, in pages. + capacity: u64, + + /// The stable memory to write data to. + memory: M, +} + +impl Default for StableIO { + fn default() -> Self { + Self::with_memory(CanisterStableMemory::default(), 0) + } +} + +impl StableIO { + /// Creates a new `StableIO` which writes to the selected memory + pub fn with_memory(memory: M, offset: u64) -> Self { + let capacity = memory.stable_size(); + Self { + offset, + capacity, + memory, + } + } + + /// Returns the offset of the writer + pub fn offset(&self) -> u64 { + self.offset + } + + /// Attempts to grow the memory by adding new pages. + pub fn grow(&mut self, new_pages: u64) -> Result<(), StableMemoryError> { + let old_page_count = self.memory.stable_grow(new_pages)?; + self.capacity = old_page_count + new_pages; + Ok(()) + } + + /// Writes a byte slice to the buffer. + /// + /// # Errors + /// + /// When it cannot grow the memory to accommodate the new data. + pub fn write(&mut self, buf: &[u8]) -> Result { + let required_capacity_bytes = self.offset + buf.len() as u64; + let required_capacity_pages = + (required_capacity_bytes + WASM_PAGE_SIZE_IN_BYTES - 1) / WASM_PAGE_SIZE_IN_BYTES; + let current_pages = self.capacity; + let additional_pages_required = required_capacity_pages.saturating_sub(current_pages); + + if additional_pages_required > 0 { + self.grow(additional_pages_required)?; + } + + self.memory.stable_write(self.offset, buf); + self.offset += buf.len() as u64; + Ok(buf.len()) + } + + /// Reads data from the stable memory location specified by an offset. + /// + /// # Errors + /// + /// The stable memory size is cached on creation of the StableReader. + /// Therefore, in following scenario, it will get an `OutOfBounds` error: + /// 1. Create a StableReader + /// 2. Write some data to the stable memory which causes it grow + /// 3. call `read()` to read the newly written bytes + pub fn read(&mut self, buf: &mut [u8]) -> Result { + let capacity_bytes = self.capacity * WASM_PAGE_SIZE_IN_BYTES; + let read_buf = if buf.len() as u64 + self.offset > capacity_bytes { + if self.offset < capacity_bytes { + // When usize=u32: + // (capacity_bytes - self.offset) < buf.len() <= u32::MAX == usize::MAX. + // So the cast below won't panic. + &mut buf[..(capacity_bytes - self.offset).try_into().unwrap()] + } else { + return Err(StableMemoryError::OutOfBounds); + } + } else { + buf + }; + self.memory.stable_read(self.offset, read_buf); + self.offset += read_buf.len() as u64; + Ok(read_buf.len()) + } + + // Helper used to implement io::Seek + fn seek(&mut self, offset: io::SeekFrom) -> io::Result { + self.offset = match offset { + io::SeekFrom::Start(offset) => offset, + io::SeekFrom::End(offset) => { + ((self.capacity * WASM_PAGE_SIZE_IN_BYTES) as i64 + offset) as u64 + } + io::SeekFrom::Current(offset) => (self.offset as i64 + offset) as u64, + }; + + Ok(self.offset) + } +} + +impl io::Write for StableIO { + fn write(&mut self, buf: &[u8]) -> Result { + self.write(buf) + .map_err(|e| io::Error::new(io::ErrorKind::OutOfMemory, e)) + } + + fn flush(&mut self) -> Result<(), io::Error> { + // Noop. + Ok(()) + } +} + +impl io::Read for StableIO { + fn read(&mut self, buf: &mut [u8]) -> Result { + Self::read(self, buf).or(Ok(0)) // Read defines EOF to be success + } +} + +impl io::Seek for StableIO { + fn seek(&mut self, offset: io::SeekFrom) -> io::Result { + self.seek(offset) + } +} + +// impl_stable_io!(u32); +// impl_stable_io!(u64); + +/// A writer to the stable memory. +/// +/// Warning: This will overwrite any existing data in stable memory as it writes, so ensure you set +/// the `offset` value accordingly if you wish to preserve existing data. +/// +/// Will attempt to grow the memory as it writes, +/// and keep offsets and total capacity. +#[derive(Debug)] +pub struct StableWriter(StableIO); + +#[allow(clippy::derivable_impls)] +impl Default for StableWriter { + #[inline] + fn default() -> Self { + Self(StableIO::default()) + } +} + +impl StableWriter { + /// Creates a new `StableWriter` which writes to the selected memory + #[inline] + pub fn with_memory(memory: M, offset: u64) -> Self { + Self(StableIO::::with_memory(memory, offset)) + } + + /// Returns the offset of the writer + #[inline] + pub fn offset(&self) -> u64 { + self.0.offset() + } + + /// Attempts to grow the memory by adding new pages. + #[inline] + pub fn grow(&mut self, new_pages: u64) -> Result<(), StableMemoryError> { + self.0.grow(new_pages) + } + + /// Writes a byte slice to the buffer. + /// + /// The only condition where this will + /// error out is if it cannot grow the memory. + #[inline] + pub fn write(&mut self, buf: &[u8]) -> Result { + self.0.write(buf) + } +} + +impl io::Write for StableWriter { + #[inline] + fn write(&mut self, buf: &[u8]) -> Result { + io::Write::write(&mut self.0, buf) + } + + #[inline] + fn flush(&mut self) -> Result<(), io::Error> { + io::Write::flush(&mut self.0) + } +} + +impl io::Seek for StableWriter { + #[inline] + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + io::Seek::seek(&mut self.0, pos) + } +} + +impl From> for StableWriter { + fn from(io: StableIO) -> Self { + Self(io) + } +} + +/// A writer to the stable memory which first writes the bytes to an in memory buffer and flushes +/// the buffer to stable memory each time it becomes full. +/// +/// Warning: This will overwrite any existing data in stable memory as it writes, so ensure you set +/// the `offset` value accordingly if you wish to preserve existing data. +/// +/// Note: Each call to grow or write to stable memory is a relatively expensive operation, so pick a +/// buffer size large enough to avoid excessive calls to stable memory. +#[derive(Debug)] +pub struct BufferedStableWriter { + inner: io::BufWriter>, +} + +impl BufferedStableWriter { + /// Creates a new `BufferedStableWriter` + pub fn new(buffer_size: usize) -> BufferedStableWriter { + BufferedStableWriter::with_writer(buffer_size, StableWriter::default()) + } +} + +impl BufferedStableWriter { + /// Creates a new `BufferedStableWriter` which writes to the selected memory + pub fn with_writer(buffer_size: usize, writer: StableWriter) -> BufferedStableWriter { + BufferedStableWriter { + inner: io::BufWriter::with_capacity(buffer_size, writer), + } + } + + /// Returns the offset of the writer + pub fn offset(&self) -> u64 { + self.inner.get_ref().offset() + } +} + +impl io::Write for BufferedStableWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.inner.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } +} + +impl io::Seek for BufferedStableWriter { + #[inline] + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + io::Seek::seek(&mut self.inner, pos) + } +} + +// A reader to the stable memory. +/// +/// Keeps an offset and reads off stable memory consecutively. +#[derive(Debug)] +pub struct StableReader(StableIO); + +#[allow(clippy::derivable_impls)] +impl Default for StableReader { + fn default() -> Self { + Self(StableIO::default()) + } +} + +impl StableReader { + /// Creates a new `StableReader` which reads from the selected memory + #[inline] + pub fn with_memory(memory: M, offset: u64) -> Self { + Self(StableIO::::with_memory(memory, offset)) + } + + /// Returns the offset of the reader + #[inline] + pub fn offset(&self) -> u64 { + self.0.offset() + } + + /// Reads data from the stable memory location specified by an offset. + /// + /// Note: + /// The stable memory size is cached on creation of the StableReader. + /// Therefore, in following scenario, it will get an `OutOfBounds` error: + /// 1. Create a StableReader + /// 2. Write some data to the stable memory which causes it grow + /// 3. call `read()` to read the newly written bytes + #[inline] + pub fn read(&mut self, buf: &mut [u8]) -> Result { + self.0.read(buf) + } +} + +impl io::Read for StableReader { + #[inline] + fn read(&mut self, buf: &mut [u8]) -> Result { + io::Read::read(&mut self.0, buf) + } +} + +impl io::Seek for StableReader { + #[inline] + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + io::Seek::seek(&mut self.0, pos) + } +} + +impl From> for StableReader { + fn from(io: StableIO) -> Self { + Self(io) + } +} + +/// A reader to the stable memory which reads bytes a chunk at a time as each chunk is required. +#[derive(Debug)] +pub struct BufferedStableReader { + inner: io::BufReader>, +} + +impl BufferedStableReader { + /// Creates a new `BufferedStableReader` + pub fn new(buffer_size: usize) -> BufferedStableReader { + BufferedStableReader::with_reader(buffer_size, StableReader::default()) + } +} + +impl BufferedStableReader { + /// Creates a new `BufferedStableReader` which reads from the selected memory + pub fn with_reader(buffer_size: usize, reader: StableReader) -> BufferedStableReader { + BufferedStableReader { + inner: io::BufReader::with_capacity(buffer_size, reader), + } + } + + /// Returns the offset of the reader + pub fn offset(&self) -> u64 { + self.inner.get_ref().offset() + } +} + +impl io::Read for BufferedStableReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.inner.read(buf) + } +} + +impl io::Seek for BufferedStableReader { + #[inline] + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + io::Seek::seek(&mut self.inner, pos) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::rc::Rc; + use std::sync::Mutex; + + #[derive(Default)] + pub struct TestStableMemory { + memory: Rc>>, + } + + impl TestStableMemory { + pub fn new(memory: Rc>>) -> TestStableMemory { + let bytes_len = memory.lock().unwrap().len(); + if bytes_len > 0 { + let pages_required = pages_required(bytes_len); + let bytes_required = pages_required * WASM_PAGE_SIZE_IN_BYTES; + memory + .lock() + .unwrap() + .resize(bytes_required.try_into().unwrap(), 0); + } + + TestStableMemory { memory } + } + } + + impl StableMemory for TestStableMemory { + fn stable_size(&self) -> u64 { + let bytes_len = self.memory.lock().unwrap().len(); + pages_required(bytes_len) + } + + fn stable_grow(&self, new_pages: u64) -> Result { + let new_bytes = new_pages * WASM_PAGE_SIZE_IN_BYTES; + + let mut vec = self.memory.lock().unwrap(); + let previous_len = vec.len() as u64; + let new_len = vec.len() as u64 + new_bytes; + vec.resize(new_len.try_into().unwrap(), 0); + Ok(previous_len / WASM_PAGE_SIZE_IN_BYTES) + } + + fn stable_write(&self, offset: u64, buf: &[u8]) { + let offset = offset as usize; + + let mut vec = self.memory.lock().unwrap(); + if offset + buf.len() > vec.len() { + panic!("stable memory out of bounds"); + } + vec[offset..(offset + buf.len())].clone_from_slice(buf); + } + + fn stable_read(&self, offset: u64, buf: &mut [u8]) { + let offset = offset as usize; + + let vec = self.memory.lock().unwrap(); + let count_to_copy = buf.len(); + + buf[..count_to_copy].copy_from_slice(&vec[offset..offset + count_to_copy]); + } + } + + fn pages_required(bytes_len: usize) -> u64 { + let page_size = WASM_PAGE_SIZE_IN_BYTES; + (bytes_len as u64 + page_size - 1) / page_size + } + + mod stable_writer_tests { + use super::*; + use rstest::rstest; + use std::io::{Seek, Write}; + + #[rstest] + #[case(None)] + #[case(Some(1))] + #[case(Some(10))] + #[case(Some(100))] + #[case(Some(1000))] + fn write_single_slice(#[case] buffer_size: Option) { + let memory = Rc::new(Mutex::new(Vec::new())); + let mut writer = build_writer(TestStableMemory::new(memory.clone()), buffer_size); + + let bytes = vec![1; 100]; + + writer.write_all(&bytes).unwrap(); + writer.flush().unwrap(); + + let result = &*memory.lock().unwrap(); + + assert_eq!(bytes, result[..bytes.len()]); + } + + #[rstest] + #[case(None)] + #[case(Some(1))] + #[case(Some(10))] + #[case(Some(100))] + #[case(Some(1000))] + fn write_many_slices(#[case] buffer_size: Option) { + let memory = Rc::new(Mutex::new(Vec::new())); + let mut writer = build_writer(TestStableMemory::new(memory.clone()), buffer_size); + + for i in 1..100 { + let bytes = vec![i as u8; i]; + writer.write_all(&bytes).unwrap(); + } + writer.flush().unwrap(); + + let result = &*memory.lock().unwrap(); + + let mut offset = 0; + for i in 1..100 { + let bytes = &result[offset..offset + i]; + assert_eq!(bytes, vec![i as u8; i]); + offset += i; + } + } + + #[rstest] + #[case(None)] + #[case(Some(1))] + #[case(Some(10))] + #[case(Some(100))] + #[case(Some(1000))] + fn ensure_only_requests_min_number_of_pages_required(#[case] buffer_size: Option) { + let memory = Rc::new(Mutex::new(Vec::new())); + let mut writer = build_writer(TestStableMemory::new(memory.clone()), buffer_size); + + let mut total_bytes = 0; + for i in 1..10000 { + let bytes = vec![i as u8; i]; + writer.write_all(&bytes).unwrap(); + total_bytes += i; + } + writer.flush().unwrap(); + + let capacity_pages = TestStableMemory::new(memory).stable_size(); + let min_pages_required = + (total_bytes as u64 + WASM_PAGE_SIZE_IN_BYTES - 1) / WASM_PAGE_SIZE_IN_BYTES; + + assert_eq!(capacity_pages, min_pages_required); + } + + #[test] + fn check_offset() { + const WRITE_SIZE: usize = 1025; + + let memory = Rc::new(Mutex::new(Vec::new())); + let mut writer = StableWriter::with_memory(TestStableMemory::new(memory.clone()), 0); + assert_eq!(writer.offset(), 0); + assert_eq!(writer.write(&vec![0; WRITE_SIZE]).unwrap(), WRITE_SIZE); + assert_eq!(writer.offset(), WRITE_SIZE as u64); + + let mut writer = BufferedStableWriter::with_writer( + WRITE_SIZE - 1, + StableWriter::with_memory(TestStableMemory::new(memory), 0), + ); + assert_eq!(writer.offset(), 0); + assert_eq!(writer.write(&vec![0; WRITE_SIZE]).unwrap(), WRITE_SIZE); + assert_eq!(writer.offset(), WRITE_SIZE as u64); + } + + #[test] + fn test_seek() { + let memory = Rc::new(Mutex::new(Vec::new())); + let mut writer = StableWriter::with_memory(TestStableMemory::new(memory.clone()), 0); + writer + .seek(std::io::SeekFrom::Start(WASM_PAGE_SIZE_IN_BYTES)) + .unwrap(); + assert_eq!(writer.stream_position().unwrap(), WASM_PAGE_SIZE_IN_BYTES); + assert_eq!(writer.write(&[1_u8]).unwrap(), 1); + assert_eq!( + writer.seek(std::io::SeekFrom::End(0)).unwrap(), + WASM_PAGE_SIZE_IN_BYTES * 2 + ); + let capacity_pages = TestStableMemory::new(memory).stable_size(); + assert_eq!(capacity_pages, 2); + } + + fn build_writer(memory: TestStableMemory, buffer_size: Option) -> Box { + let writer = StableWriter::with_memory(memory, 0); + if let Some(buffer_size) = buffer_size { + Box::new(BufferedStableWriter::with_writer(buffer_size, writer)) + } else { + Box::new(writer) + } + } + } + + mod stable_reader_tests { + use super::*; + use rstest::rstest; + use std::io::{Read, Seek}; + + #[rstest] + #[case(None)] + #[case(Some(1))] + #[case(Some(10))] + #[case(Some(100))] + #[case(Some(1000))] + fn reads_all_bytes(#[case] buffer_size: Option) { + let input = vec![1; 10_000]; + let memory = Rc::new(Mutex::new(input.clone())); + let mut reader = build_reader(TestStableMemory::new(memory), buffer_size); + + let mut output = Vec::new(); + reader.read_to_end(&mut output).unwrap(); + + assert_eq!(input, output[..input.len()]); + } + + #[test] + fn check_offset() { + const READ_SIZE: usize = 1025; + + let memory = Rc::new(Mutex::new(vec![1; READ_SIZE])); + let mut reader = StableReader::with_memory(TestStableMemory::new(memory.clone()), 0); + assert_eq!(reader.offset(), 0); + let mut bytes = vec![0; READ_SIZE]; + assert_eq!(reader.read(&mut bytes).unwrap(), READ_SIZE); + assert_eq!(reader.offset(), READ_SIZE as u64); + + let mut reader = BufferedStableReader::with_reader( + READ_SIZE - 1, + StableReader::with_memory(TestStableMemory::new(memory), 0), + ); + assert_eq!(reader.offset(), 0); + let mut bytes = vec![0; READ_SIZE]; + assert_eq!(reader.read(&mut bytes).unwrap(), READ_SIZE); + assert_eq!(reader.offset(), READ_SIZE as u64); + } + + #[test] + fn test_seek() { + const SIZE: usize = 1025; + let memory = Rc::new(Mutex::new((0..SIZE).map(|v| v as u8).collect::>())); + let mut reader = StableReader::with_memory(TestStableMemory::new(memory), 0); + let mut bytes = vec![0_u8; 1]; + + const OFFSET: usize = 200; + reader + .seek(std::io::SeekFrom::Start(OFFSET as u64)) + .unwrap(); + assert_eq!(reader.stream_position().unwrap() as usize, OFFSET); + assert_eq!(reader.read(&mut bytes).unwrap(), 1); + assert_eq!(&bytes, &[OFFSET as u8]); + assert_eq!( + reader.seek(std::io::SeekFrom::End(0)).unwrap(), + WASM_PAGE_SIZE_IN_BYTES + ); + reader + .seek(std::io::SeekFrom::Start(WASM_PAGE_SIZE_IN_BYTES * 2)) + .unwrap(); + // out of bounds so should fail + assert!(reader.read(&mut bytes).is_err()); + } + + fn build_reader(memory: TestStableMemory, buffer_size: Option) -> Box { + let reader = StableReader::with_memory(memory, 0); + if let Some(buffer_size) = buffer_size { + Box::new(BufferedStableReader::with_reader(buffer_size, reader)) + } else { + Box::new(reader) + } + } + } +} diff --git a/ic-cdk/src/storage.rs b/ic-cdk/src/storage.rs index fe8d81451..c582a3ee4 100644 --- a/ic-cdk/src/storage.rs +++ b/ic-cdk/src/storage.rs @@ -1,5 +1,5 @@ //! Tools for managing stable storage of data in a canister. -use crate::api::stable; +use crate::stable; /// Saves the storage into the stable memory. ///