Canister Calls

So far we’ve been calling canister methods from the command-line with dfx. Canisters can also call canister methods. Such calls behave similarly to asynchronous calls in JavaScript.

In particular, code that calls other canisters provide reply and reject callbacks, one of which is invoked when the call eventually finishes. These callbacks are entries in the WebAssembly table. Motoko and the Rust devkit take care of these details, but as we’re doing our own stunts, we must find some way of getting at the wasm table.

It appears Clang conveniently compiles function pointers to table indexes. We rely on this behaviour. We use integer casts to avoid compiler warnings.

[The first time around, I worked too hard: I wrote a special tool that took a wasm binary as input, and looked for the exported functions "table 0", "table 1", …. It then wrote a version of the binary that starts off with these functions in its table.]

Example

We demonstrate canister-calling code by getting a canister to call itself:

#define IMPORT(m,n) __attribute__((import_module(m))) __attribute__((import_name(n)));
#define EXPORT(n) asm(n) __attribute__((visibility("default")))
typedef unsigned u32;
u32 self_size(void)               IMPORT("ic0", "canister_self_size");
void self_copy(void*, u32, u32)   IMPORT("ic0", "canister_self_copy");
void reply_append(void*, u32)     IMPORT("ic0", "msg_reply_data_append");
void reply(void)                  IMPORT("ic0", "msg_reply");
u32 call_perform()                IMPORT("ic0", "call_perform");
void call_new(void*, u32, void*, u32, u32, u32, u32, u32)
    IMPORT("ic0", "call_new");

unsigned char counter;

void inc() EXPORT("canister_update inc");
void inc() {
  static char empty[] = "DIDL\x00\x00";
  counter++;
  reply_append(empty, 6);
  reply();
}

void get() EXPORT("canister_query get");
void get() {
  static char buf[] = "DIDL\x00\x01\x7b";  // One nat8 argument.
  buf[7] = counter;
  reply_append(buf, 8);
  reply();
}

void reply_callback(u32 whatever) {
  counter += 4;
}

void reject_callback(u32 whatever) {
}

void call_inc() EXPORT("canister_update call_inc");
void call_inc() {
  char self[64];
  u32 n = self_size();
  self_copy(self, 0, n);
  call_new(self, n, "inc", 3, (u32) reply_callback, 1234, (u32) reject_callback, 5678);
  call_perform();
  static char empty[] = "DIDL\x00\x00";
  reply_append(empty, 6);
  reply();
}

Compiling and feeding the resulting binary to our above tool produces an appropriate table and elem section, which we can confirm with wasm2wat:

  (table (;0;) 2 funcref)
  (export "table 0" (func 9))
  (export "table 1" (func 10))
  (elem (;0;) (i32.const 0) func 9 10)

Like the Rust counter canister example, the get and inc methods read and increment a global counter, replying in Candid:

$ dfx canister call counter get
(0 : nat8)
$ dfx canister call counter inc
()
$ dfx canister call counter get
(1 : nat8)

The star of the show is call_inc, which makes an asychronous call to inc, and on success, adds 4 to the counter.

The first step is ic0.call_new. The first 2 arguments are the target canister, which is itself. We use the syscalls ic0.canister_self_size and ic0.canister_self_copy to learn the address of the canister (its principal ID).

The next 2 arguments describe the method we’re calling, namely inc.

The last 4 arguments are the reply and reject callbacks. We refer to each by their index in the table. Each also takes a 32-bit integer argument. Our canister is simple so has no use for this. In more complex applications, this number may record the context of the call.

The reply callback increments the counter by 4 and the reject callback does nothing. We test the happy path:

$ dfx canister call counter get
(1 : nat8)
$ dfx canister call counter call_inc
()
$ dfx canister call counter get
(6 : nat8)

As expected, the counter increases by 5: the call to itself causes it to go up by 1, and then the reply callback bumps it up further by 4.

As calls are asynchronous, calls to call_inc and their reply callbacks might be interleaved. For example, two call_inc calls might increment by 1 twice before incrementing by 4 twice. This is harmless for our simple counter demo, but in general, we must handle concurrency correctly.

(Be aware some system calls such as ic0.caller_size and ic0.reply trap if called in the callbacks; I found out the hard way!)


💡