diff --git a/README.md b/README.md index 93d1caa..9de0dd5 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ Add this to your `Cargo.toml` [dependencies] ic-kit = "0.5.0-alpha.4" candid = "0.7" + +[features] +kit-lib = [] ``` ## Example Usage diff --git a/examples/README.md b/examples/README.md index 964300b..05f45d6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,5 +4,40 @@ This directory contains some example canister's implemented using the IC Kit. Ea to be as simple as possible to demonstrate one aspect of the IC Kit and a possible design pattern you can also use to develop canisters. -Simple State Manipulation: -- Counter \ No newline at end of file +# Testing the examples + +```shell +cd ./fib +cargo test +``` + +# Build example + +```shell +cd ./fib +cargo build --target wasm32-unknown-unknown --release --features kit-wasm-export +``` + +> Without `kit-wasm-export` the canister method will not be part of the public interface of the generated WASM. In simple +> terms, the code will compile but the methods won't be there! + +# About Cargo.toml + +Since the canister's developed by IC-Kit are intended to be imported as normal Rust crates, we don't want to generate +the wasm bindings by default, so we should explicitly specify the `kit-wasm-export` feature when building the crate as a +canister. + +This prevents any unintentional name collisions, which can result in catastrophic events if you're building a canister. +For example if you depend on the interface of a `Counter` if that crate implements a `increment` method, now your +canister will also have the `increment` method, resulting in unintended use cases. + +```toml +[features] +kit-lib = [] + +[lib] +crate-type = ["cdylib", "lib"] +``` + +The `cdylib` is used, so we can target for `wasm32-unknown-unknown`, the `lib` is used to make it possible to import the +canister as a crate. diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 7ad73c8..a063f3e 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -8,6 +8,8 @@ edition = "2021" [dependencies] ic-kit = {path="../../ic-kit"} -[[bin]] -name = "ic_kit_example_counter" -path = "src/main.rs" +[features] +kit-lib = [] + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/examples/counter/src/canister.rs b/examples/counter/src/canister.rs index 5f780b2..a4e8b00 100644 --- a/examples/counter/src/canister.rs +++ b/examples/counter/src/canister.rs @@ -37,6 +37,7 @@ pub fn get_counter(counter: &Counter) -> u64 { #[derive(KitCanister)] #[candid_path("candid.did")] +#[wasm_path("../../target/wasm32-unknown-unknown/ic_kit_example_counter.wasm")] pub struct CounterCanister; #[cfg(test)] diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs deleted file mode 100644 index c53437a..0000000 --- a/examples/counter/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod canister; - -fn main() {} diff --git a/examples/factory_counter/Cargo.toml b/examples/factory_counter/Cargo.toml index d3762eb..3fb9259 100644 --- a/examples/factory_counter/Cargo.toml +++ b/examples/factory_counter/Cargo.toml @@ -7,8 +7,10 @@ edition = "2021" [dependencies] ic-kit = {path="../../ic-kit"} -ic_kit_example_counter = {path="../counter"} +ic_kit_example_counter = {path="../counter", features=["kit-lib"]} -[[bin]] -name = "ic_kit_example_factory_counter" -path = "src/main.rs" +[features] +kit-lib = [] + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/examples/factory_counter/src/canister.rs b/examples/factory_counter/src/canister.rs index 06e36dc..975badf 100644 --- a/examples/factory_counter/src/canister.rs +++ b/examples/factory_counter/src/canister.rs @@ -1,14 +1,73 @@ use ic_kit::prelude::*; use ic_kit_example_counter::CounterCanister; +#[cfg(target_family = "wasm")] +fn deploy(_id: Principal) { + unimplemented!() +} + +#[cfg(not(target_family = "wasm"))] +fn deploy(id: Principal) { + use ic_kit::rt::Canister; + + let canister: Canister = CounterCanister::build(id); + let canister = Box::leak(Box::new(canister)); + + CallBuilder::new(Principal::management_canister(), "ic_kit_install") + .with_arg(unsafe { + let ptr = canister as *mut Canister as *mut _ as usize; + ptr + }) + .perform_one_way() + .expect("ic-kit: could not install dynamic canister."); +} + #[update] async fn deploy_counter() -> Principal { - // TODO(qti3e) we should make it possible to deploy an instance of CounterCanister. - // It should work in the IC and the ic-kit-runtime environment. - // CounterCanister::install_code(CANISTER_ID); - todo!() + println!("Deploy counter!"); + + let id = Principal::from_text("whq4n-xiaaa-aaaam-qaazq-cai").unwrap(); + deploy::(id); + + id } #[derive(KitCanister)] #[candid_path("candid.did")] pub struct FactoryCounterCanister; + +#[cfg(test)] +mod tests { + use super::*; + + #[kit_test] + async fn x(replica: Replica) { + let factory = replica.add_canister(FactoryCounterCanister::anonymous()); + let new_canister_id = factory + .new_call("deploy_counter") + .perform() + .await + .decode_one::() + .unwrap(); + + let counter = replica.get_canister(new_canister_id); + let r = counter + .new_call("increment") + .perform() + .await + .decode_one::() + .unwrap(); + + assert_eq!(r, 1); + + assert_eq!( + counter + .new_call("get_counter") + .perform() + .await + .decode_one::() + .unwrap(), + 1 + ); + } +} diff --git a/examples/factory_counter/src/main.rs b/examples/factory_counter/src/main.rs deleted file mode 100644 index 8fc52ee..0000000 --- a/examples/factory_counter/src/main.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod canister; -fn main() {} diff --git a/examples/fib/Cargo.toml b/examples/fib/Cargo.toml index 083f3b2..2963640 100644 --- a/examples/fib/Cargo.toml +++ b/examples/fib/Cargo.toml @@ -8,6 +8,8 @@ edition = "2021" [dependencies] ic-kit = {path="../../ic-kit"} -[[bin]] -name = "ic_kit_example_fib" -path = "src/main.rs" +[features] +kit-lib = [] + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/examples/fib/src/main.rs b/examples/fib/src/main.rs deleted file mode 100644 index 8fc52ee..0000000 --- a/examples/fib/src/main.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod canister; -fn main() {} diff --git a/examples/multi_counter/Cargo.toml b/examples/multi_counter/Cargo.toml index 2ec2d94..517f494 100644 --- a/examples/multi_counter/Cargo.toml +++ b/examples/multi_counter/Cargo.toml @@ -7,8 +7,10 @@ edition = "2021" [dependencies] ic-kit = {path="../../ic-kit"} -ic_kit_example_counter = {path="../counter"} +ic_kit_example_counter = {path="../counter", features=["kit-lib"]} -[[bin]] -name = "ic_kit_example_multi_counter" -path = "src/main.rs" +[features] +kit-lib = [] + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/examples/multi_counter/src/canister.rs b/examples/multi_counter/src/canister.rs index ec9d386..0412f9e 100644 --- a/examples/multi_counter/src/canister.rs +++ b/examples/multi_counter/src/canister.rs @@ -1,6 +1,8 @@ use ic_kit::prelude::*; use std::collections::HashSet; +use ic_kit_example_counter::CounterCanister; + #[derive(Default)] struct MultiCounter { canister_ids: HashSet, @@ -32,7 +34,9 @@ pub struct MultiCounterCanister; #[cfg(test)] mod tests { use super::*; - use ic_kit_example_counter::CounterCanister; + + #[test] + fn x() {} #[kit_test] async fn test_multi_canister(replica: Replica) { diff --git a/examples/multi_counter/src/main.rs b/examples/multi_counter/src/main.rs deleted file mode 100644 index 8fc52ee..0000000 --- a/examples/multi_counter/src/main.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod canister; -fn main() {} diff --git a/examples/naming_system/Cargo.toml b/examples/naming_system/Cargo.toml index 7831ace..a05cf27 100644 --- a/examples/naming_system/Cargo.toml +++ b/examples/naming_system/Cargo.toml @@ -8,6 +8,8 @@ edition = "2021" [dependencies] ic-kit = {path="../../ic-kit"} -[[bin]] -name = "ic_kit_example_naming_system" -path = "src/main.rs" +[features] +kit-lib = [] + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/examples/naming_system/src/main.rs b/examples/naming_system/src/main.rs deleted file mode 100644 index 8fc52ee..0000000 --- a/examples/naming_system/src/main.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod canister; -fn main() {} diff --git a/ic-kit-macros/src/entry.rs b/ic-kit-macros/src/entry.rs index 93921cc..f4494d9 100644 --- a/ic-kit-macros/src/entry.rs +++ b/ic-kit-macros/src/entry.rs @@ -148,6 +148,11 @@ pub fn gen_entry_point_code( Span::call_site(), ); + let export_function_ident = Ident::new( + &format!("_ic_kit_exported_canister_{}_{}", entry_point, name), + Span::call_site(), + ); + let guard = if let Some(guard_name) = attrs.guard { let guard_ident = Ident::new(&guard_name, Span::call_site()); @@ -299,9 +304,8 @@ pub fn gen_entry_point_code( } } - #[cfg(target_family = "wasm")] #[doc(hidden)] - #[export_name = #export_name] + #[inline(always)] fn #outer_function_ident() { #[cfg(target_family = "wasm")] ic_kit::setup_hooks(); @@ -310,14 +314,11 @@ pub fn gen_entry_point_code( #body } - #[cfg(not(target_family = "wasm"))] + #[cfg(not(feature="kit-lib"))] + #[export_name = #export_name] #[doc(hidden)] - fn #outer_function_ident() { - #[cfg(target_family = "wasm")] - ic_kit::setup_hooks(); - - #guard - #body + fn #export_function_ident() { + #outer_function_ident(); } #[inline(always)] diff --git a/ic-kit-macros/src/export_service.rs b/ic-kit-macros/src/export_service.rs index 90871b8..2c6429f 100644 --- a/ic-kit-macros/src/export_service.rs +++ b/ic-kit-macros/src/export_service.rs @@ -4,7 +4,7 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use std::collections::BTreeMap; use std::sync::Mutex; -use syn::{DeriveInput, Error}; +use syn::Error; struct Method { hidden: bool, @@ -86,7 +86,17 @@ pub(crate) fn declare( Ok(()) } -pub fn export_service(input: DeriveInput, save_candid_path: Option) -> TokenStream { +pub struct ExportServiceConfig { + pub name: Ident, + pub save_candid_path: Option, + pub wasm_path: Option, +} + +pub fn export_service(config: ExportServiceConfig) -> TokenStream { + let name = config.name; + let save_candid_path = config.save_candid_path; + let wasm_path = config.wasm_path; + let methods = { let mut map = METHODS.lock().unwrap(); std::mem::replace(&mut *map, BTreeMap::new()) @@ -180,8 +190,6 @@ pub fn export_service(input: DeriveInput, save_candid_path: Option) quote! { let actor = Some(ty); } }; - let name = input.ident; - let save_candid = if let Some(path) = save_candid_path { quote! { #[cfg(test)] @@ -193,7 +201,7 @@ pub fn export_service(input: DeriveInput, save_candid_path: Option) use std::path::PathBuf; let candid = #name::candid(); - let mut path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push(#path); let dir = path.parent().unwrap(); @@ -220,6 +228,25 @@ pub fn export_service(input: DeriveInput, save_candid_path: Option) quote! {} }; + let dynamic_canister = if let Some(path) = wasm_path { + quote! { + #[cfg(feature="kit-lib")] + impl ic_kit::KitDynamicCanister for #name { + #[cfg(not(target_family = "wasm"))] + fn get_canister_wasm() -> &'static [u8] { + panic!("WASM is not available during test.") + } + + #[cfg(target_family = "wasm")] + fn get_canister_wasm() -> &'static [u8] { + include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #path)) + } + } + } + } else { + quote! {} + }; + quote! { impl ic_kit::KitCanister for #name { #[cfg(not(target_family = "wasm"))] @@ -238,7 +265,9 @@ pub fn export_service(input: DeriveInput, save_candid_path: Option) } } - #[cfg(target_family = "wasm")] + #dynamic_canister + + #[cfg(not(feature="kit-lib"))] #[doc(hidden)] #[export_name = "canister_query __get_candid_interface_tmp_hack"] fn _ic_kit_canister_query___get_candid_interface_tmp_hack() { diff --git a/ic-kit-macros/src/lib.rs b/ic-kit-macros/src/lib.rs index 17f1afe..3d25e41 100644 --- a/ic-kit-macros/src/lib.rs +++ b/ic-kit-macros/src/lib.rs @@ -67,28 +67,37 @@ pub fn kit_test(attr: TokenStream, item: TokenStream) -> TokenStream { .into() } -#[proc_macro_derive(KitCanister, attributes(candid_path))] +#[proc_macro_derive(KitCanister, attributes(candid_path, wasm_path))] pub fn kit_export(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as syn::DeriveInput); - let save_candid_path_result = get_save_candid_path(&input); - match save_candid_path_result { - Ok(save_candid_path) => export_service::export_service(input, save_candid_path).into(), - Err(e) => e.to_compile_error().into(), - } + let save_candid_path = match get_attribute_lit(&input, "candid_path") { + Ok(attr) => attr, + Err(e) => return e.to_compile_error().into(), + }; + + let wasm_path = match get_attribute_lit(&input, "wasm_path") { + Ok(attr) => attr, + Err(e) => return e.to_compile_error().into(), + }; + + let config = export_service::ExportServiceConfig { + name: input.ident, + save_candid_path, + wasm_path, + }; + + export_service::export_service(config).into() } -fn get_save_candid_path(input: &syn::DeriveInput) -> syn::Result> { - let candid_path_helper_attribute_option = input +fn get_attribute_lit( + input: &syn::DeriveInput, + attr_name: &str, +) -> syn::Result> { + input .attrs .iter() - .find(|attr| attr.path.is_ident("candid_path")); - - match candid_path_helper_attribute_option { - Some(candid_path_helper_attribute) => { - let custom_candid_path_lit: syn::LitStr = candid_path_helper_attribute.parse_args()?; - Ok(Some(custom_candid_path_lit)) - } - None => Ok(None), - } + .find(|attr| attr.path.is_ident(attr_name)) + .map(|attr| attr.parse_args::()) + .map_or(Ok(None), |e| e.map(Some)) } diff --git a/ic-kit-runtime/src/call.rs b/ic-kit-runtime/src/call.rs index c691f3a..d4f96de 100644 --- a/ic-kit-runtime/src/call.rs +++ b/ic-kit-runtime/src/call.rs @@ -101,12 +101,12 @@ impl<'a> CallBuilder<'a> { impl CallReply { /// Convert the reply to a message that can be delivered to a canister. - pub(crate) fn to_message(self, reply_to: OutgoingRequestId) -> Message { + pub(crate) fn to_message(self, reply_to: OutgoingRequestId) -> CanisterMessage { match self { CallReply::Reply { data, cycles_refunded, - } => Message::Reply { + } => CanisterMessage::Reply { reply_to, env: Env::default() .with_entry_mode(EntryMode::ReplyCallback) @@ -117,7 +117,7 @@ impl CallReply { rejection_code, rejection_message, cycles_refunded, - } => Message::Reply { + } => CanisterMessage::Reply { reply_to, env: Env::default() .with_entry_mode(EntryMode::RejectCallback) diff --git a/ic-kit-runtime/src/canister.rs b/ic-kit-runtime/src/canister.rs index 20bb8b3..adfc04a 100644 --- a/ic-kit-runtime/src/canister.rs +++ b/ic-kit-runtime/src/canister.rs @@ -202,7 +202,7 @@ impl Canister { pub async fn process_message( &mut self, - message: Message, + message: CanisterMessage, reply_sender: Option>, ) -> Vec { // Force reset the state. @@ -213,7 +213,7 @@ impl Canister { // Assign the request_id for this message. let (request_id, env, task) = match message { - Message::CustomTask { + CanisterMessage::CustomTask { request_id, env, task, @@ -230,7 +230,7 @@ impl Canister { (request_id, env, Some(task)) } - Message::Request { request_id, env } => { + CanisterMessage::Request { request_id, env } => { assert!( reply_sender.is_some(), "A request must provide a response channel." @@ -257,7 +257,7 @@ impl Canister { (request_id, env, task) } - Message::Reply { reply_to, env } => { + CanisterMessage::Reply { reply_to, env } => { let callbacks = self.outgoing_calls.remove(&reply_to).expect( "ic-kit-runtime: No outgoing message with the given id on this canister.", ); diff --git a/ic-kit-runtime/src/handle.rs b/ic-kit-runtime/src/handle.rs index 08b75df..b831b70 100644 --- a/ic-kit-runtime/src/handle.rs +++ b/ic-kit-runtime/src/handle.rs @@ -1,5 +1,5 @@ use crate::call::{CallBuilder, CallReply}; -use crate::types::{Env, Message, RequestId}; +use crate::types::{CanisterMessage, Env, RequestId}; use crate::Replica; use ic_types::Principal; use std::panic::{RefUnwindSafe, UnwindSafe}; @@ -26,7 +26,7 @@ impl<'a> CanisterHandle<'a> { self.replica.enqueue_request( self.canister_id, - Message::CustomTask { + CanisterMessage::CustomTask { request_id: RequestId::new(), task: Box::new(f), env, @@ -43,7 +43,7 @@ impl<'a> CanisterHandle<'a> { self.replica.enqueue_request( self.canister_id, - Message::Request { + CanisterMessage::Request { request_id: RequestId::new(), env, }, diff --git a/ic-kit-runtime/src/replica.rs b/ic-kit-runtime/src/replica.rs index 4c7913d..34bfe2a 100644 --- a/ic-kit-runtime/src/replica.rs +++ b/ic-kit-runtime/src/replica.rs @@ -1,9 +1,8 @@ //! Implementation of a Internet Computer's replica actor model. A replica can contain any number of -//! canisters and a canister, user should be able to send messages to a canister and await for the -//! response of the call. And the canister's should also be able to send messages to another canister. +//! canisters, a user should be able to send messages to a canister and await for the response of +//! the call. And the any canister should also be able to send messages to another canister. //! -//! Different canister should operate in parallel, but each canister can only process one request -//! at a time. +//! Different canister operate in parallel, but each canister can only process one request at a time. //! //! In this implementation this is done by starting different event loops for each canister and doing //! cross worker communication using Tokio's mpsc channels, the Replica object itself does not hold @@ -17,48 +16,61 @@ use crate::call::{CallBuilder, CallReply}; use crate::canister::Canister; use crate::handle::CanisterHandle; use crate::types::*; +use candid::decode_one; use ic_kit_sys::types::RejectionCode; use ic_types::Principal; -use std::collections::HashMap; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; use std::future::Future; -use std::panic::{RefUnwindSafe, UnwindSafe}; use tokio::sync::{mpsc, oneshot}; +thread_local! { + static REPLICA: RefCell>> = RefCell::new(None); +} + /// A local replica that contains one or several canisters. pub struct Replica { // The current implementation uses a `tokio::spawn` to run an event loop for the replica, // the state of the replica is store in that event loop. - sender: mpsc::UnboundedSender, + sender: mpsc::UnboundedSender, } /// The state of the replica, it does not live inside the replica itself, but an instance of it /// is created in the replica worker, and messages from the `Replica` are transmitted to this /// object using an async channel. -#[derive(Default)] struct ReplicaState { + /// The worker to the current replica state. + sender: mpsc::UnboundedSender, /// Map each of the current canisters to the receiver of that canister's event loop. - canisters: HashMap>, + canisters: HashMap>, + /// The reserved canister principal ids. + created: HashSet, } -/// A message that Replica wants to send to a canister to be processed. -struct ReplicaCanisterRequest { - message: Message, - reply_sender: Option>, +/// A message received by the canister worker. +enum CanisterWorkerMessage { + Message { + message: CanisterMessage, + reply_sender: Option>, + }, } -enum ReplicaMessage { - CanisterAdded { - canister_id: Principal, - channel: mpsc::UnboundedSender, +/// A message received by the replica worker. +enum ReplicaWorkerMessage { + CreateCanister { + reply_sender: oneshot::Sender, + }, + InstallCode { + canister: Canister, }, CanisterRequest { canister_id: Principal, - message: Message, + message: CanisterMessage, reply_sender: Option>, }, CanisterReply { canister_id: Principal, - message: Message, + message: CanisterMessage, }, } @@ -78,21 +90,10 @@ impl Replica { pub fn add_canister(&self, canister: Canister) -> CanisterHandle { let canister_id = canister.id(); - // Create a execution queue for the canister so we can send messages to the canister - // asynchronously - let replica = self.sender.clone(); - - let (tx, rx) = mpsc::unbounded_channel(); - replica - .send(ReplicaMessage::CanisterAdded { - canister_id, - channel: tx, - }) + self.sender + .send(ReplicaWorkerMessage::InstallCode { canister }) .unwrap_or_else(|_| panic!("ic-kit-runtime: could not send message to replica")); - // Start the event loop for the canister. - tokio::spawn(canister_worker(rx, replica, canister)); - CanisterHandle { replica: self, canister_id, @@ -111,11 +112,11 @@ impl Replica { pub(crate) fn enqueue_request( &self, canister_id: Principal, - message: Message, + message: CanisterMessage, reply_sender: Option>, ) { self.sender - .send(ReplicaMessage::CanisterRequest { + .send(ReplicaWorkerMessage::CanisterRequest { canister_id, message, reply_sender, @@ -127,12 +128,16 @@ impl Replica { /// call is executed. pub(crate) fn perform_call(&self, call: CanisterCall) -> impl Future { let canister_id = call.callee; - let message = Message::from(call); + let message = CanisterMessage::from(call); let (tx, rx) = oneshot::channel(); self.enqueue_request(canister_id, message, Some(tx)); async { - rx.await - .expect("ic-kit-runtime: Could not retrieve the response from the call.") + rx.await.unwrap_or_else(|e| { + panic!( + "ic-kit-runtime: Could not retrieve the response from the call. {:?}", + e + ) + }) } } @@ -146,28 +151,48 @@ impl Replica { impl Default for Replica { /// Create an empty replica and run the start the event loop. fn default() -> Self { - let (sender, rx) = mpsc::unbounded_channel::(); - tokio::spawn(replica_worker(rx)); + let (sender, rx) = mpsc::unbounded_channel::(); + tokio::spawn(replica_worker(sender.clone(), rx)); Replica { sender } } } /// Run replica's event loop, gets ReplicaMessages and performs the state transition accordingly. -async fn replica_worker(mut rx: mpsc::UnboundedReceiver) { - let mut state = ReplicaState::default(); +async fn replica_worker( + sender: mpsc::UnboundedSender, + mut rx: mpsc::UnboundedReceiver, +) { + let mut state = ReplicaState { + sender, + canisters: Default::default(), + created: Default::default(), + }; + + // Run the management canister, this will make the reset of the code to work as if the management + // canister is actually a canister. + { + let (tx, rx) = mpsc::unbounded_channel(); + tokio::spawn(management_canister_worker(state.sender.clone(), rx)); + state.canisters.insert(Principal::management_canister(), tx); + } while let Some(message) = rx.recv().await { match message { - ReplicaMessage::CanisterAdded { - canister_id, - channel, - } => state.canister_added(canister_id, channel), - ReplicaMessage::CanisterRequest { + ReplicaWorkerMessage::CreateCanister { reply_sender } => { + let id = state.create_canister(); + reply_sender + .send(id) + .expect("Could not send back to result for the canister create request."); + } + ReplicaWorkerMessage::InstallCode { canister } => { + state.install_code(canister); + } + ReplicaWorkerMessage::CanisterRequest { canister_id, message, reply_sender, } => state.canister_request(canister_id, message, reply_sender), - ReplicaMessage::CanisterReply { + ReplicaWorkerMessage::CanisterReply { canister_id, message, } => state.canister_reply(canister_id, message), @@ -178,75 +203,161 @@ async fn replica_worker(mut rx: mpsc::UnboundedReceiver) { /// Start a dedicated event loop for a canister, this will get CanisterMessage messages from a tokio /// channel and perform async fn canister_worker( - mut rx: mpsc::UnboundedReceiver, - mut replica: mpsc::UnboundedSender, + mut rx: mpsc::UnboundedReceiver, + mut replica: mpsc::UnboundedSender, mut canister: Canister, +) { + while let Some(message) = rx.recv().await { + match message { + CanisterWorkerMessage::Message { + message, + reply_sender, + } => perform_canister_request(&mut canister, &mut replica, message, reply_sender).await, + }; + } +} + +/// Run a request against the canister. +async fn perform_canister_request( + canister: &mut Canister, + replica: &mut mpsc::UnboundedSender, + message: CanisterMessage, + reply_sender: Option>, ) { let canister_id = canister.id(); - let mut rx = rx; - let mut canister = canister; + // Perform the message on the canister's thread, the result containing a list of + // inter-canister call requests is returned here, so we can send each call back to + // replica. + let canister_requested_calls = canister.process_message(message, reply_sender).await; + + for call in canister_requested_calls { + // For each call a oneshot channel is created that is used to receive the response + // from the target canister. We then await for the response in a `tokio::spawn` to not + // block the current queue. Once the response is received we send it back as a + // `CanisterReply` back to the replica so it can perform the routing and send the + // response. + // This of course could be avoided if a sender to the same rx was passed to this method. + // TODO(qti3e) Do the optimization - we don't need to send the result to the replica + // just so that it queues to our own `rx`. + let request_id = call.request_id; + let (tx, rx) = oneshot::channel(); - while let Some(message) = rx.recv().await { - // Perform the message on the canister's thread, the result containing a list of - // inter-canister call requests is returned here, so we can send each call back to - // replica. - let canister_requested_calls = canister - .process_message(message.message, message.reply_sender) - .await; - - for call in canister_requested_calls { - // For each call a oneshot channel is created that is used to receive the response - // from the target canister. We then await for the response in a `tokio::spawn` to not - // block the current queue. Once the response is received we send it back as a - // `CanisterReply` back to the replica so it can perform the routing and send the - // response. - // This of course could be avoided if a sender to the same rx was passed to this method. - // TODO(qti3e) Do the optimization - we don't need to send the result to the replica - // just so that it queues to our own `rx`. - let request_id = call.request_id; - let (tx, rx) = oneshot::channel(); + replica + .send(ReplicaWorkerMessage::CanisterRequest { + canister_id: call.callee, + message: call.into(), + reply_sender: Some(tx), + }) + .unwrap_or_else(|_| panic!("ic-kit-runtime: could not send message to replica")); + + let rs = replica.clone(); + + tokio::spawn(async move { + let replica = rs; + + // wait for the response from the destination canister. + let response = rx + .await + .expect("ic-kit-runtime: Could not get the response of inter-canister call."); + let message = response.to_message(request_id); + + // once we have the result send it as a request to the current canister. replica - .send(ReplicaMessage::CanisterRequest { - canister_id: call.callee, - message: call.into(), - reply_sender: Some(tx), + .send(ReplicaWorkerMessage::CanisterReply { + canister_id, + message, }) .unwrap_or_else(|_| panic!("ic-kit-runtime: could not send message to replica")); + }); + } +} - let rs = replica.clone(); +/// Run the worker for the management canister. +async fn management_canister_worker( + mut replica: mpsc::UnboundedSender, + mut rx: mpsc::UnboundedReceiver, +) { + while let Some(message) = rx.recv().await { + match message { + CanisterWorkerMessage::Message { + message: CanisterMessage::Request { env, .. }, + reply_sender: Some(sender), + } => { + let reply = handle_management_method(&mut replica, env).await; + sender + .send(reply) + .expect("ic-kit-runtime: could not send management response."); + } + CanisterWorkerMessage::Message { + reply_sender: Some(sender), + .. + } => sender + .send(CallReply::Reject { + rejection_code: RejectionCode::Unknown, + rejection_message: "ic-kit-runtime: unexpected call to management canister." + .to_string(), + cycles_refunded: 0, + }) + .expect("ic-kit-runtime: could not send management response."), + _ => panic!("Unexpected call to management canister."), + }; + } +} - tokio::spawn(async move { - let replica = rs; +async fn handle_management_method( + replica: &mut mpsc::UnboundedSender, + env: Env, +) -> CallReply { + let method_name = env.method_name.unwrap(); + println!("mgmt - {}", method_name); - // wait for the response from the destination canister. - let response = rx - .await - .expect("ic-kit-runtime: Could not get the response of inter-canister call."); + if method_name == "ic_kit_install" { + let arg = decode_one::(env.args.as_slice()).unwrap(); + let canister = unsafe { + let ptr = arg as *mut u8 as *mut Canister; + *Box::from_raw(ptr) + }; - let message = response.to_message(request_id); + replica + .send(ReplicaWorkerMessage::InstallCode { canister }) + .unwrap_or_else(|_| panic!("ic-kit-runtime: could not send message to replica")); + } - // once we have the result send it as a request to the current canister. - replica - .send(ReplicaMessage::CanisterReply { - canister_id, - message, - }) - .unwrap_or_else(|_| { - panic!("ic-kit-runtime: could not send message to replica") - }); - }); - } + CallReply::Reply { + data: vec![], + cycles_refunded: 0, } } impl ReplicaState { - pub fn canister_added( - &mut self, - canister_id: Principal, - channel: mpsc::UnboundedSender, - ) { + /// Return the first unused canister id. + fn get_next_canister_id(&mut self) -> Principal { + let mut id = self.created.len() as u64; + + loop { + let canister_id = canister_id(id); + + if !self.canisters.contains_key(&canister_id) { + break canister_id; + } + + id += 1; + } + } + + /// Create a new canister by reserving a canister id. + pub fn create_canister(&mut self) -> Principal { + let canister_id = self.get_next_canister_id(); + self.created.insert(canister_id); + canister_id + } + + /// Install the given canister. + pub fn install_code(&mut self, canister: Canister) { + let canister_id = canister.id(); + if self.canisters.contains_key(&canister_id) { panic!( "Canister '{}' is already defined in the replica.", @@ -254,26 +365,29 @@ impl ReplicaState { ) } - self.canisters.insert(canister_id, channel); + let (tx, rx) = mpsc::unbounded_channel(); + tokio::spawn(canister_worker(rx, self.sender.clone(), canister)); + + self.canisters.insert(canister_id, tx); } pub fn canister_request( &mut self, canister_id: Principal, - message: Message, + message: CanisterMessage, reply_sender: Option>, ) { if let Some(chan) = self.canisters.get(&canister_id) { - chan.send(ReplicaCanisterRequest { + chan.send(CanisterWorkerMessage::Message { message, reply_sender, }) .unwrap_or_else(|_| panic!("ic-kit-runtime: Could not enqueue the request.")); } else { let cycles_refunded = match message { - Message::CustomTask { env, .. } => env.cycles_available, - Message::Request { env, .. } => env.cycles_refunded, - Message::Reply { .. } => 0, + CanisterMessage::CustomTask { env, .. } => env.cycles_available, + CanisterMessage::Request { env, .. } => env.cycles_refunded, + CanisterMessage::Reply { .. } => 0, }; reply_sender @@ -287,12 +401,39 @@ impl ReplicaState { } } - fn canister_reply(&mut self, canister_id: Principal, message: Message) { + fn canister_reply(&mut self, canister_id: Principal, message: CanisterMessage) { let chan = self.canisters.get(&canister_id).unwrap(); - chan.send(ReplicaCanisterRequest { + chan.send(CanisterWorkerMessage::Message { message, reply_sender: None, }) .unwrap_or_else(|_| panic!("ic-kit-runtime: Could not enqueue the response request.")); } } + +const fn canister_id(id: u64) -> Principal { + let mut data = [0_u8; 10]; + + // Specify explicitly the length, so as to assert at compile time that a u64 + // takes exactly 8 bytes + let val: [u8; 8] = id.to_be_bytes(); + + // for-loops in const fn are not supported + data[0] = val[0]; + data[1] = val[1]; + data[2] = val[2]; + data[3] = val[3]; + data[4] = val[4]; + data[5] = val[5]; + data[6] = val[6]; + data[7] = val[7]; + + // Even though not defined in the interface spec, add another 0x1 to the array + // to create a sub category that could be used in future. + data[8] = 0x01; + + // Add the Principal's TYPE_OPAQUE tag. + data[9] = 0x01; + + Principal::from_slice(&data) +} diff --git a/ic-kit-runtime/src/types.rs b/ic-kit-runtime/src/types.rs index c41278f..04f7847 100644 --- a/ic-kit-runtime/src/types.rs +++ b/ic-kit-runtime/src/types.rs @@ -70,7 +70,7 @@ pub type TaskFn = Box; /// A message sent to a canister that trigger execution of a task on the canister's execution thread /// based on the type of the message. -pub enum Message { +pub enum CanisterMessage { /// A custom function that you want to be executed in the canister's execution thread. CustomTask { /// The request id of this incoming message. @@ -109,9 +109,9 @@ pub struct CanisterCall { pub arg: Vec, } -impl From for Message { +impl From for CanisterMessage { fn from(call: CanisterCall) -> Self { - Message::Request { + CanisterMessage::Request { request_id: call.request_id, env: Env::default() .with_entry_mode(EntryMode::Update) diff --git a/ic-kit/src/canister.rs b/ic-kit/src/canister.rs index 67f8322..31fe0dc 100644 --- a/ic-kit/src/canister.rs +++ b/ic-kit/src/canister.rs @@ -1,3 +1,25 @@ +use crate::Principal; +use candid::CandidType; +use ic_kit_sys::types::CallError; +use serde::{Deserialize, Serialize}; +use std::future::Future; + +// TODO(qti3e) Move this to management module. +#[derive(Debug, Clone, PartialOrd, PartialEq, CandidType, Serialize, Deserialize)] +pub enum InstallMode { + Install, + Reinstall, + Upgrade, +} + +#[derive(Debug, Clone, PartialOrd, PartialEq, CandidType, Serialize)] +pub struct InstallCodeArgument { + pub mode: InstallMode, + pub canister_id: Principal, + pub wasm_module: &'static [u8], + pub arg: Vec, +} + /// A canister. pub trait KitCanister { /// Create a new instance of this canister using the provided canister id. @@ -13,3 +35,17 @@ pub trait KitCanister { /// The candid description of the canister. fn candid() -> String; } + +/// A dynamic canister is a canister that can be dynamically created and installed. +pub trait KitDynamicCanister: KitCanister { + /// Should return the wasm binary of the canister. + fn get_canister_wasm() -> &'static [u8]; + + #[cfg(not(target_family = "wasm"))] + fn install_code( + canister_id: Principal, + mode: InstallMode, + ) -> Box>> { + todo!() + } +} diff --git a/ic-kit/src/lib.rs b/ic-kit/src/lib.rs index ebe518d..891abd8 100644 --- a/ic-kit/src/lib.rs +++ b/ic-kit/src/lib.rs @@ -18,7 +18,7 @@ pub use ic_kit_macros as macros; pub use setup::setup_hooks; // The KitCanister derive macro. -pub use canister::KitCanister; +pub use canister::{InstallMode, KitCanister, KitDynamicCanister}; pub use ic_kit_macros::KitCanister; /// The IC-kit runtime, which can be used for testing the canister in non-wasm environments. @@ -27,7 +27,7 @@ pub use ic_kit_runtime as rt; /// The famous prelude module which re exports the most useful methods. pub mod prelude { - pub use super::canister::KitCanister; + pub use super::canister::{KitCanister, KitDynamicCanister}; pub use super::ic; pub use super::ic::CallBuilder; pub use super::ic::{balance, caller, id, spawn};