#define IMPORT(m,n) __attribute__((import_module(m))) __attribute__((import_name(n)));
#define EXPORT(n) asm(n) __attribute__((visibility("default")))
void reply_append(void*, unsigned) IMPORT("ic0", "msg_reply_data_append");
void reply (void) IMPORT("ic0", "msg_reply");
void go() EXPORT("canister_query hi");
void go() {
char msg[] = "Hello, World!\n";
reply_append(msg, sizeof(msg) - 1);
reply();
}
Hello, Internet Computer!
A canister is a WebAssembly binary. Thankfully, the non-standard wasm extensions in early prototypes of the IC were ultimately abandoned, so off-the-shelf tooling just works.
WebAssembly is explained in many other places (plug: see my introduction to WebAssembly), so we only mention that:
-
An IC canister imports various functions from the
ic0module. Instead of callingprintfto say something, we might callic0.msg_reply_data_appendandic0.msg_reply. -
An IC canister exports functions with names like
canister_query fooandcanister_update bar. These are called viadfx, or via HTTPS, or by other canisters.
A query is a cheap, read-only call: our code is allowed to modify global variables and memory, but afterwards the changes are reverted. In contrast, the changes made by an update are permanent.
That’s all we need to know to write our first canister, hello.c:
Build
Turning this into a wasm binary would simply be a matter of running clang
--target=wasm32 if it weren’t for some linker options we need:
-
--no-entry: there’s no_startsymbol in our file. -
--export-dynamic: export all our functions. -
--allow-undefined: avoid complaints about the missingic0functions.
We run:
$ clang --target=wasm32 -c -O3 hello.c $ wasm-ld --no-entry --export-dynamic --allow-undefined hello.o -o hello.wasm
to produce hello.wasm, which we examine with wasm2wat:
$ wasm2wat hello.wasm
(module
(type (;0;) (func (param i32 i32)))
(type (;1;) (func))
(import "ic0" "msg_reply_data_append" (func $reply_append (type 0)))
(import "ic0" "msg_reply" (func $reply (type 1)))
(func $canister_query_hi (type 1)
(local i32)
global.get 0
i32.const 16
i32.sub
local.tee 0
global.set 0
local.get 0
i32.const 0
i64.load offset=1031 align=1
i64.store offset=7 align=1
local.get 0
i32.const 0
i64.load offset=1024 align=1
i64.store
local.get 0
i32.const 14
call $reply_append
call $reply
local.get 0
i32.const 16
i32.add
global.set 0)
(memory (;0;) 2)
(global (;0;) (mut i32) (i32.const 66576))
(export "memory" (memory 0))
(export "canister_query hi" (func $canister_query_hi))
(data (;0;) (i32.const 1024) "Hello, World!\0a\00"))
(Not too shabby, but we could save a few bytes by removing the global and the NUL in the string.)
Deploy
We start a local test net and deploy our canister:
$ echo service:{} > did.not
$ cat > dfx.json << EOF
{"canisters":{"hello":
{"type":"custom"
,"build":""
,"candid":"did.not"
,"wasm":"hello.wasm"
}}}
EOF
$ dfx start
$ dfx deploy
Above, each entry in the JSON file must have 4 fields:
-
type: we set this tocustomto preventdfxautomatically invoking the Motoko or Rust compiler. -
build: iftypeiscustom, thendfxruns this command to build the canister. We leave it blank so it does nothing; we already built our canister manually. -
candid: we’ll explain this another time. For now, this must refer to an empty file. -
wasm: the wasm binary we created.
We test the hi query method:
$ dfx canister call hello hi --output raw 48656c6c6f2c20576f726c6421
The output is dumps our string in hex, which we confirm:
$ dfx canister call hello hi --output raw | xxd -r -p Hello, World!
To deploy to production, we would add the --network ic option.
TL;DR
We recap the above in the following bash script mkhello.sh, which creates
hello.c, compiles it to WebAssembly, deploys it to a local net, and then
calls its hi method.
#!/usr/bin/env bash
set -euxo pipefail
cat > hello.c << EOF
#define IMPORT(m,n) __attribute__((import_module(m))) __attribute__((import_name(n)));
#define EXPORT(n) asm(n) __attribute__((visibility("default")))
void reply_append(void*, unsigned) IMPORT("ic0", "msg_reply_data_append");
void reply (void) IMPORT("ic0", "msg_reply");
void go() EXPORT("canister_query hi");
void go() {
char msg[] = "Hello, World!\n";
reply_append(msg, sizeof(msg) - 1);
reply();
}
EOF
clang --target=wasm32 -c -O3 hello.c
wasm-ld --no-entry --export-dynamic --allow-undefined hello.o -o hello.wasm
echo service:{} > did.not
cat > dfx.json << EOF
{"canisters":{"hello":
{"type":"custom"
,"build":""
,"candid":"did.not"
,"wasm":"hello.wasm"
}}}
EOF
dfx deploy
dfx canister call hello hi --output raw | xxd -r -p
It assumes a local net is already running. To start one up, type the following:
echo {} > dfx.json; dfx start
Then in another terminal, download and run the bash script:
bash <(curl -s https://fxa77-fiaaa-aaaae-aaana-cai.raw.ic0.app/mkhello.sh)