Rust toolkit for Internet Computer canister development. Standardizes common patterns: authentication, HTTP handling, storage, telemetry, inter-canister calls, and ML model serving.
Released via git tags (not yet on crates.io). Add to your Cargo.toml:
[dependencies]
ic-dev-kit-rs = { git = "https://github.com/DrJesseGlass/ic-dev-kit-rs", tag = "v0.2.0" }
# Enable optional features (most common)
ic-dev-kit-rs = { git = "https://github.com/DrJesseGlass/ic-dev-kit-rs", tag = "v0.2.0", features = ["storage", "telemetry"] }
# ML features (includes storage automatically)
ic-dev-kit-rs = { git = "https://github.com/DrJesseGlass/ic-dev-kit-rs", tag = "v0.2.0", features = ["text-generation"] }Note on ML features + wasm: the candle/text-generation features pull in
getrandom, which has no default backend on wasm32-unknown-unknown. Canister
projects using these features must build with
RUSTFLAGS='--cfg getrandom_backend="custom"' (e.g. in .cargo/config.toml)
and register a custom entropy source (IC canisters typically seed from
raw_rand).
| Feature | Description | Dependencies |
|---|---|---|
storage |
Stable storage utilities | ic-stable-structures |
telemetry |
Canistergeek monitoring/logging | canistergeek_ic_rust |
candle |
ML model infrastructure | candle-core, candle-nn |
text-generation |
LLM text generation | candle, tokenizers |
use ic_dev_kit_rs::auth;
#[ic_cdk::init]
fn init() {
// Initialize auth with deployer as first authorized principal
auth::init_with_caller();
}
#[ic_cdk::update(guard = "auth::is_authorized")]
fn protected_method() {
// Only authorized principals can call this
}
#[ic_cdk::update(guard = "auth::is_authorized")]
fn add_admin(principal: Principal) {
auth::add_principal(principal).unwrap();
}use ic_dev_kit_rs::http::{self, HttpRequest, HttpResponse, HttpError};
#[ic_cdk::query]
fn http_request(req: HttpRequest) -> HttpResponse {
let path = http::extract_path(&req.url);
match (req.method.as_str(), path) {
("GET", "/api/status") => {
http::success_response(&serde_json::json!({"status": "ok"})).unwrap()
}
("POST", "/api/data") => {
match http::parse_json::<MyData>(&req.body) {
Ok(data) => http::success_response(&data).unwrap(),
Err(e) => e.to_response(),
}
}
_ => HttpError::NotFound.to_response(),
}
}use ic_dev_kit_rs::storage::{self, StorageRegistry};
use ic_stable_structures::{StableBTreeMap, memory_manager::*, DefaultMemoryImpl};
use std::cell::RefCell;
type Memory = VirtualMemory<DefaultMemoryImpl>;
thread_local! {
static MEMORY_MANAGER: RefCell<MemoryManager<DefaultMemoryImpl>> =
RefCell::new(MemoryManager::init(DefaultMemoryImpl::default()));
static REGISTRY: RefCell<StableBTreeMap<String, Vec<u8>, Memory>> = RefCell::new(
StableBTreeMap::init(
MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(1))),
)
);
}
// Save any CandidType
#[ic_cdk::update]
fn save_config(config: MyConfig) -> Result<(), String> {
REGISTRY.with(|reg| {
storage::save_candid(reg, "config", &config)
})
}
// Load it back
#[ic_cdk::query]
fn get_config() -> Option<MyConfig> {
REGISTRY.with(|reg| {
storage::load_candid(reg, "config")
})
}
// Raw bytes for binary data
#[ic_cdk::update]
fn save_binary(key: String, data: Vec<u8>) {
REGISTRY.with(|reg| {
storage::save_bytes(reg, &key, data);
});
}use ic_dev_kit_rs::telemetry;
#[ic_cdk::init]
fn init() {
telemetry::init();
}
#[ic_cdk::update]
fn process_data() {
telemetry::collect_metrics();
telemetry::log_info("Processing started");
// Your logic here...
telemetry::log_info("Processing completed");
}
// Use the macro to export Canistergeek-compatible endpoints
ic_dev_kit_rs::export_telemetry_endpoints!();Buffers are keyed by an owner principal, so concurrent uploads from
different callers are isolated from each other. Total buffered bytes per
owner are capped (2 GiB by default, configurable via
large_objects::set_max_bytes_per_owner). Buffers live on the Wasm heap —
finalize uploads before upgrading the canister.
use ic_dev_kit_rs::large_objects;
// Sequential upload (simple, chunks must arrive in order)
#[ic_cdk::update]
fn upload_chunk(data: Vec<u8>) -> Result<usize, String> {
large_objects::append_chunk(ic_cdk::api::msg_caller(), data)
}
#[ic_cdk::update]
fn finalize_upload() -> Vec<u8> {
large_objects::get_buffer_data(ic_cdk::api::msg_caller())
}
// Parallel upload (faster, chunks can arrive out of order)
#[ic_cdk::update]
fn upload_parallel_chunk(chunk_id: u32, data: Vec<u8>) -> Result<usize, String> {
large_objects::append_parallel_chunk(ic_cdk::api::msg_caller(), chunk_id, data)
}
#[ic_cdk::query]
fn check_upload_complete(expected_count: u32) -> bool {
large_objects::parallel_chunks_complete(ic_cdk::api::msg_caller(), expected_count)
}
#[ic_cdk::query]
fn get_missing_chunks(expected_count: u32) -> Vec<u32> {
large_objects::missing_chunks(ic_cdk::api::msg_caller(), expected_count)
}
#[ic_cdk::update]
fn finalize_parallel_upload() -> Result<Vec<u8>, String> {
let owner = ic_cdk::api::msg_caller();
large_objects::consolidate_parallel_chunks(owner)?;
Ok(large_objects::get_buffer_data(owner))
}
#[ic_cdk::query]
fn upload_status() -> String {
large_objects::storage_status(ic_cdk::api::msg_caller()).to_string()
}Or generate all of the above (plus optional storage integration) with the
macro — it passes msg_caller() as the owner automatically:
ic_dev_kit_rs::generate_upload_endpoints!(guard = "auth::is_authorized");use ic_dev_kit_rs::intercanister;
use candid::Principal;
#[ic_cdk::update]
async fn call_other_canister(canister_id: Principal) -> Result<String, String> {
// Simple call with automatic logging
intercanister::call(canister_id, "get_data", ()).await
}
#[ic_cdk::update]
async fn call_with_args(canister_id: Principal, arg: String) -> Result<u64, String> {
// Call with arguments (use tuple for multiple args)
intercanister::call(canister_id, "process", (arg,)).await
}
#[ic_cdk::update]
async fn call_with_cycles(canister_id: Principal) -> Result<String, String> {
// Call with cycles attached
intercanister::call_with_payment(
canister_id,
"paid_method",
(),
1_000_000, // cycles
).await
}
#[ic_cdk::update]
fn fire_and_forget(canister_id: Principal) -> Result<(), String> {
// One-way notification (no response)
intercanister::call_one_way(canister_id, "log_event", ("user_action",))
}All modules support canister upgrades:
use ic_dev_kit_rs::auth;
#[cfg(feature = "telemetry")]
use ic_dev_kit_rs::telemetry;
// Store bytes in stable memory (use ic-stable-structures or similar)
thread_local! {
static AUTH_BACKUP: RefCell<Vec<u8>> = RefCell::new(Vec::new());
#[cfg(feature = "telemetry")]
static TELEMETRY_BACKUP: RefCell<Vec<u8>> = RefCell::new(Vec::new());
}
#[ic_cdk::pre_upgrade]
fn pre_upgrade() {
AUTH_BACKUP.with(|b| *b.borrow_mut() = auth::save_to_bytes());
#[cfg(feature = "telemetry")]
TELEMETRY_BACKUP.with(|b| *b.borrow_mut() = telemetry::save_to_bytes());
}
#[ic_cdk::post_upgrade]
fn post_upgrade() {
let auth_data = AUTH_BACKUP.with(|b| b.borrow().clone());
auth::init_from_saved(if auth_data.is_empty() { None } else { Some(auth_data) });
#[cfg(feature = "telemetry")]
{
let telemetry_data = TELEMETRY_BACKUP.with(|b| b.borrow().clone());
telemetry::init_from_bytes(if telemetry_data.is_empty() { None } else { Some(telemetry_data) });
}
}| Function | Description |
|---|---|
init() |
Initialize with empty auth |
init_with_caller() |
Initialize with deployer authorized |
init_with_principals(Vec<Principal>) |
Initialize with specific principals |
init_from_saved(Option<Vec<u8>>) |
Restore from saved bytes |
is_authorized() -> Result<(), String> |
Guard function for IC CDK |
add_principal(Principal) |
Add authorized principal |
remove_principal(Principal) |
Remove authorized principal |
list_principals() |
List all authorized principals |
save_to_bytes() -> Vec<u8> |
Serialize for upgrade |
Types: HttpRequest, HttpResponse, HttpError, HttpMethod, Router
| Function | Description |
|---|---|
parse_json<T>(&[u8]) |
Parse request body as JSON |
success_response<T>(&T) |
Create 200 JSON response |
error_response(u16, &str) |
Create error response |
json_response(u16, String) |
Create JSON response with status |
extract_path(&str) |
Extract path from URL |
extract_query_params(&str) |
Extract query parameters |
extract_params(&str, &str) |
Extract path parameters from pattern |
matches_pattern(&str, &str) |
Check if path matches pattern |
get_header(&[(String,String)], &str) |
Get header (case-insensitive) |
extract_bearer_token(&[(String,String)]) |
Extract Bearer token |
| Function | Description |
|---|---|
save_candid<T>(registry, key, &T) |
Save any CandidType |
load_candid<T>(registry, key) |
Load any CandidType |
save_bytes(registry, key, Vec<u8>) |
Save raw bytes |
load_bytes(registry, key) |
Load raw bytes |
delete(registry, key) |
Delete entry |
exists(registry, key) |
Check if key exists |
size(registry, key) |
Get size in bytes |
All functions take an owner: Principal as their first argument (use
ic_cdk::api::msg_caller() in endpoints); each owner gets isolated buffers.
| Function | Description |
|---|---|
append_chunk(owner, Vec<u8>) |
Add to sequential buffer (checks cap) |
buffer_size(owner) |
Get sequential buffer size |
get_buffer_data(owner) |
Get and clear sequential buffer |
clear_buffer(owner) |
Clear sequential buffer |
append_parallel_chunk(owner, u32, Vec<u8>) |
Add chunk with ID (checks cap) |
parallel_chunk_count(owner) |
Get parallel chunk count |
parallel_chunks_complete(owner, u32) |
Check all chunks received |
missing_chunks(owner, u32) |
Get missing chunk IDs |
consolidate_parallel_chunks(owner) |
Merge parallel to sequential |
get_parallel_data(owner) |
Get parallel data without moving |
clear_parallel_chunks(owner) |
Clear parallel buffer |
storage_status(owner) |
Get detailed status |
total_buffered_bytes(owner) |
Combined buffered bytes for owner |
set_max_bytes_per_owner(Option<usize>) |
Set per-owner byte cap (None = unlimited) |
max_bytes_per_owner() |
Get the current cap |
| Function | Description |
|---|---|
call<T,R>(Principal, &str, T) |
Async call with logging |
call_with_payment<T,R>(Principal, &str, T, u128) |
Call with cycles |
call_one_way<T>(Principal, &str, T) |
Fire-and-forget notification |
call_no_args<R>(Principal, &str) |
Call with no arguments |
| Function | Description |
|---|---|
init() |
Initialize telemetry |
collect_metrics() |
Collect canister metrics |
log_info(msg) |
Log info message |
log_warning(msg) |
Log warning message |
log_error(msg) |
Log error message |
log_debug(msg) |
Log debug message |
is_monitoring_authorized() |
Guard for monitoring endpoints |
add_monitoring_principal(Principal) |
Add monitoring access |
save_to_bytes() |
Serialize for upgrade |
init_from_bytes(Option<Vec<u8>>) |
Restore from saved bytes |
| Macro | Description |
|---|---|
export_auth_endpoints!() |
Generate auth management endpoints |
export_telemetry_endpoints!() |
Generate Canistergeek endpoints |
generate_upload_endpoints!(...) |
Generate upload endpoints |
generate_model_endpoints!(...) |
Generate ML inference endpoints |
See the examples directory for complete canister examples.
MIT OR Apache-2.0