Hello, Candid!

Why have we been supplying the option --output raw? What if we omit it?

$ dfx canister call hello hi
Error deserializing blob 0x48656c6c6f2c20576f726c64210a
Error: Invalid data: Invalid IDL blob.
Caused by:
  Cannot parse header 48656c6c6f2c20576f726c64210a
    binary parser error: Unexpected bytes at byte offset 0

We see raw bytes are considered uncouth by the dfx tool. In polite company, one should encode one’s output in the Candid format.

We walk through the encoding of "Hello, Candid!\n":

  • "DIDL": a magic string heralds the arrival of a Candid message.

  • 0x01: number of entries in the type table.

  • 0x6d 0x7b: the first (and only) entry of the type table; 0x6d means "vector", and 0x7b means "8-bit naturals", which C programmers know as uint8_t, and is more widely known as a "byte" or "octet". Altogether, this means "vector of bytes", which we also call a "blob".

  • 0x01: number of arguments.

  • 0x00: the first (and only) argument has the type of index 0, that is, the first entry of the type table, namely, a blob.

  • 0x0f: vector of size 15.

  • "Hello, Candid!\n": the items of the vector.

Those familiar with the wasm binary format may experience deja vu:

  • Both begin with a 4-byte magic string.

  • Both encode the size of a table before the encoding each entry of the table.

  • Both have one section for types and another for things referring back to the type table by index.

  • Both count backwards from 0x7f to get the constants representing primitive types.

  • Both are little-endian, and use the LEB128 format extensively.

This is because Candid and WebAssembly share a key designer.

We tweak our previous canister, yielding hellocandid.c:

#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[] = "DIDL\x01\x6d\x7b\x01\x00\x0fHello, Candid!\n";
  reply_append(msg, sizeof(msg) - 1);
  reply();
}

We repeat the steps from before to build and deploy this canister. This time, there’s no need for the --output flag:

$ dfx canister call hellocandid hi
(blob "Hello, Candid!\0a")

We can confirm the output really is Candid-encoded:

$ dfx canister call hellocandid hi --output raw
4449444c016d7b01000f48656c6c6f2c2043616e646964210a

Blobs Versus Text

The intent of our canister is to output a message for human consumption, so it may be better to use the dedicated text type rather than a blob:

  • "DIDL": a magic string heralds the arrival of a Candid message.

  • 0x00: number of entries in the following type table; zero means the type table is empty.

  • 0x01: one argument to follow.

  • 0x71: the first (and only) argument has type text (0x71), namely a UTF-8 string.

  • 0x0f: text of length 15.

  • "Hello, Candid!\n": the items of the vector.

This time we get away without a type table because the text type is primitive so may directly appear in the argument section. (Vector types are not primitive, so must be declared in the type table and referred to by their index.)

We leave it as an exercise to change the message to:

char msg[] = "DIDL\x00\x01\x71\x0fHello, Candid!\n";

and confirm that calling the hi method results in:

("Hello, Candid!\n")

Speaking Candidly

Remember that candid field in dfx.json that we’ve been pointing to an empty file? It’s really meant to be a file describing a Candid interface of the canister. The dfx tool refers to it when makng calls.

In our above example, we might declare:

service HelloCandid : {
  hi : () -> (blob);
}

A Candid interface file also acts as documentation.

See also:


💡