Build your home on the Internet Computer!

Install and run:

  $ git clone https://fxa77-fiaaa-aaaae-aaana-cai.raw.ic0.app/monic.git
  $ cd monic
  $ ./monic monic.html
  $ dfx deploy --network ic

Copy the resulting ID into a browser, and append .raw.ic0.app. For example: abcde-fghij-aaaaa-aaada-cai.raw.ic0.app. This README should appear.

Replace monic.html with your own content, and re-deploy. Repeat as needed.

How it works

We walk through the source of a minimal web app:

#define WASM_IMPORT(m,n) __attribute__((import_module(m))) __attribute__((import_name(n)));
#define WASM_EXPORT(n) asm(n) __attribute__((visibility("default")))

void reply_data_append(void *, int) WASM_IMPORT("ic0", "msg_reply_data_append");
void reply(void) WASM_IMPORT("ic0", "msg_reply");
void go() WASM_EXPORT("canister_query http_request");
void go() {
  char msg[] = "DIDL\x03"
      "\x6c\x03"
        "\xa2\xf5\xed\x88\x04\x01"
        "\xc6\xa4\xa1\x98\x06\x02"
        "\x9a\xa1\xb2\xf9\x0c\x7a"
      "\x6d\x7b"
      "\x6d\x6f"
      "\x01\x00\x08Hi, all!\x00\xc8\x00";
  reply_data_append(msg, sizeof(msg) - 1);
  reply();
}

An HTTP request to ...raw.ic0.app becomes a wasm call to the exported function canister_query http_request. We ignore the contents of the request, and always give the same response.

To respond to a request, our wasm should call the imported function ic0.msg_reply after calling ic0.msg_reply_data_append to append a serialized response in Candid format:

pub struct HttpResponse {
    pub status_code: u16,
    pub headers: Vec<(String, String)>,
    pub body: ByteBuf,
    pub streaming_strategy: Option<StreamingStrategy>,
}

A Candid Response

Accordingly, our response starts with Candid’s magic header:

DIDL  -- Abracadabra!

Next is the type table:

0x03  -- 3 types. We index them from 0.
  0x6c 0x03  -- Type #0 : record with 3 fields
    0xa2 0xf5 0xed 0x88 0x04 : 0x01  -- H("body") : Type #1
    0xc6 0xa4 0xa1 0x98 0x06 : 0x02  -- H("headers") : Type #2
    0x9a 0xa1 0xb2 0xf9 0x0c : 0x7a  -- H("status_code") : Nat16
  0x6d 0x7b  -- Type #1 : Vec Nat8
  0x6d 0x6f  -- Type #2 : Vec Empty

The hash function H takes a UTF-8 string, views each byte as an integer modulo 232, then runs the following function:

h :: [Word32] -> Word32
h s = sum $ zipWith (*) (reverse s) $ iterate (223*) 1

The resulting 32-bit hashes are LEB128-encoded. (Candid has no record field names; only hashes. Collisions can occur, but are easy to avoid in practice.)

Because we send an empty header vector, Candid lets us get away with declaring headers as a Vec Empty instead of Vec (String, String).

We omit the optional streaming_strategy field so we may skip declaring its type.

The arguments follow the type table:

0x01  -- 1 argument.
  0x00 -- Type #0, namely the record declared above.
    0x08 "Hi, all!"  -- Vec Nat8 of length 8, with contents "Hi, all!".
    0x00  -- Vec of length 0.
    0xc8 0x00  -- LEB128 encoding of the Nat16 200.

In other words, we respond with the body "Hi, all!", no headers, and the status code 200.

The monic script embeds the supplied file in a C file similar to the above.

By the way, https://fxa77-fiaaa-aaaae-aaana-cai.raw.ic0.app/explain decodes and explains Candid blobs in a similar manner to above.

Off script

The rest of the script automates the following.

Producing a wasm32 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 _start symbol in our file.

  • --export-dynamic: export all our functions.

  • --allow-undefined: avoid complaints about the missing ic0 functions.

wcc="clang --target=wasm32 -c -O3"
wld="wasm-ld-11 --no-entry --export-dynamic --allow-undefined"
$wcc hi.c
$wld hi.o -o hi.wasm

This produces a 454-byte hi.wasm.

Next, we need a Candid declaration file, even if it contains nothing:

touch hi.did

We create a dfx.json with custom settings:

{"canisters":{"hi":{
  "type":"custom"
  "candid":"hi.did"
  "wasm":"hi.wasm"
  "build":""
}}}

And off we go!

dfx deploy --network ic

If we put our build commands in a script, and replace the build field with the script’s name, then future dfx deploy invocations automatically build our app.