A QR Code Canister

Since we’re using a stock C compiler, we can readily turn any C project into a canister provided it is mostly self-contained. A good example is Project Nayuki’s QR Code generator library.

Let’s make a canister that uses this library to generate QR codes.

Trial and Error

We try compiling the C version of this library to wasm:

$ git clone https://github.com/nayuki/QR-Code-generator
$ cd QR-Code-generator/c
$ clang --target=wasm32 -std=c99 -c qrcodegen.c
qrcodegen.c:24:10: fatal error: 'assert.h' file not found
#include <assert.h>
         ^~~~~~~~~~
1 error generated.

A missing assert.h is easy to work around:

$ echo '#define assert(x) ((void)(0))' > assert.h

But then we find we’re missing stdlib.h:

$ clang -isystem . --target=wasm32 -std=c99 -c qrcodegen.c
qrcodegen.c:26:10: fatal error: 'assert.h' file not found
#include <stdlib.h>
         ^~~~~~~~~~
1 error generated.

The quickest way to progress:

$ touch stdlib.h
$ clang -isystem . --target=wasm32 -std=c99 -c qrcodegen.c

It then complains about string.h, so:

$ touch string.h
$ clang -isystem . --target=wasm32 -std=c99 -c qrcodegen.c

The error messages are a bit of a mouthful. The salient parts:

$ clang -isystem . --target=wasm32 -std=c99 -c qrcodegen.c 2>&1 | grep note
qrcodegen.c:132:19: note: include the header <string.h> or explicitly provide a declaration for 'strlen'
qrcodegen.c:231:2: note: include the header <string.h> or explicitly provide a declaration for 'memset'
qrcodegen.c:390:3: note: include the header <string.h> or explicitly provide a declaration for 'memmove'
qrcodegen.c:466:15: note: include the header <stdlib.h> or explicitly provide a declaration for 'abs'
qrcodegen.c:706:17: note: include the header <stdlib.h> or explicitly provide a declaration for 'labs'
qrcodegen.c:824:7: note: include the header <string.h> or explicitly provide a declaration for 'strchr'
qrcodegen.c:884:3: note: include the header <string.h> or explicitly provide a declaration for 'memcpy'

We tell the compiler what it wants to hear:

int abs(int);
long labs(long);
#include <stddef.h>
size_t strlen(const char*);
void *memset(void *, int, size_t);
void *memchr(const void *, int, size_t);
char *strchr(const char *, int);
void* memmove(void *, const void *, size_t);
void* memcpy(void *, const void *, size_t);
int memcmp(const void *, const void *, size_t);

It compiles!

$ clang -isystem . --target=wasm32 -std=c99 -c qrcodegen.c

We implement the functions we declared. We take this opportunity to declare and define memcmp, because we’ll need it in a later app.

#include <stddef.h>
size_t strlen(const char* s) {
  const char *p = s;
  while (*p) p++;
  return p - s;
}
void *memset(void *s, int c, size_t n) {
  char *p = s;
  while (n--) *p++ = c;
  return s;
}
char *strchr(const char *p, int c) {
  while(*p) if (*p++ == c) return (char *)p;
  return 0;
}
void *memchr(const void *s, int c, size_t n) {
  const char *p = s;
  while(n--) if (*p++ == c) return (void *)p;
  return 0;
}
void* memcpy(void *dst, const void *src, size_t n) {
  char *p = dst;
  const char *s = src;
  while (n--) *p++ = *s++;
  return dst;
}
void* memmove(void *dst, const void *src, size_t n) {
  char *d = dst;
  const char *s = src;
  if (d <= s) {
    while (n--) *d++ = *s++;
  } else {
    s += n;
    d += n;
    while (n--) *d-- = *s--;
  }
  return dst;
}
int memcmp(const void *s1, const void *s2, size_t n) {
  for(size_t i = 0; i < n; i++) {
    const unsigned char* l = s1;
    const unsigned char* r = s2;
    if (l[i] < r[i]) return -1;
    if (l[i] > r[i]) return 1;
  }
  return 0;
}
int abs(int n) { return n < 0 ? -n : n; }
long labs(long n) { return n < 0 ? -n : n; }

Trivia: according to the C standard, our memmove() implementation involves undefined behaviour. Fortunately, no self-respecting compiler respects that part of the standard!

We compile our homebrew version of a tiny excerpt of the C standard library to wasm:

$ clang --target=wasm32 -std=c99 -c libc.c

[Back when I ran Clang 8, I didn’t have to reinvent as much of the standard C library. My guess is that the old version would use the standard system include files even if it was targeting wasm32.]

Canister Source

Now we can write a canister using this library. We copy doBasicDemo() from the library test program qrcodegen-demo.c and modify it for the IC: we replace fputs with reply_append, and call ic0.msg_arg_data_size and ic0.msg_arg_data_copy to copy the input argument to the buffer buf.

#include "qrcodegen.h"

#define IMPORT(m,n) __attribute__((import_module(m))) __attribute__((import_name(n)));
#define EXPORT(n) asm(n) __attribute__((visibility("default")))
typedef unsigned u32;
void reply_append(void*, u32)  IMPORT("ic0", "msg_reply_data_append");
void reply(void)               IMPORT("ic0", "msg_reply");
u32 arg_size(void)             IMPORT("ic0", "msg_arg_data_size");
void arg_copy(void*, u32, u32) IMPORT("ic0", "msg_arg_data_copy");

void printQr(const uint8_t qrcode[]) {
  int size = qrcodegen_getSize(qrcode);
  int border = 4;
  for (int y = -border; y < size + border; y++) {
    for (int x = -border; x < size + border; x++) {
      reply_append(qrcodegen_getModule(qrcode, x, y) ? "##" : "  ", 2);
    }
    reply_append("\n", 1);
  }
  reply_append("\n", 1);
}

void basic(const char *text) {
  enum qrcodegen_Ecc errCorLvl = qrcodegen_Ecc_LOW;
  uint8_t qrcode[qrcodegen_BUFFER_LEN_MAX];
  uint8_t tempBuffer[qrcodegen_BUFFER_LEN_MAX];
  bool ok = qrcodegen_encodeText(text, tempBuffer, qrcode, errCorLvl,
    qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
  if (ok)
    printQr(qrcode);
}

enum { max = 2048 };

void go() EXPORT("canister_update go");
void go() {
  u32 n = arg_size();
  n = n > max ? max : n;
  char buf[max + 1];
  arg_copy(buf, 0, n);
  buf[n] = 0;
  basic(buf);
  reply();
}

We compile and link with the other object files:

$ clang --target=wasm32 -std=c99 -c qr.c
$ wasm-ld --no-entry --export-dynamic --allow-undefined libc.o qrcodegen.o qr.o -o qr.wasm

As before, we add a suitable canisters entry to dfx.json:

{"qr":
  {"type":"custom"
  ,"build":""
  ,"candid":"did.not"
  ,"wasm":"qr.wasm"
}}

where did.not is an empty file, and run dfx deploy.

We now have a canister that produces a QR code from a given NUL-terminated string. We test it, mindful that in raw mode, both input and output are hex-enocded:

$ dfx canister call qr go --type raw `echo "Hello, QR Code!" | xxd -p` --output raw | xxd -r -p




        ##############  ######  ##  ##############
        ##          ##      ####    ##          ##
        ##  ######  ##  ##  ####    ##  ######  ##
        ##  ######  ##  ####    ##  ##  ######  ##
        ##  ######  ##  ##########  ##  ######  ##
        ##          ##              ##          ##
        ##############  ##  ##  ##  ##############
                          ####
        ########    ##  ##  ####  ##    ######  ##
            ####  ##  ##  ####            ####  ##
        ####  ##  ######  ##      ######      ####
              ######      ##            ####  ##
          ####  ########      ######  ##        ##
                        ##  ######  ##  ##  ##  ##
        ##############    ######  ####  ##
        ##          ##        ##      ##  ####
        ##  ######  ##      ##  ##    ########  ##
        ##  ######  ##  ######  ####      ####
        ##  ######  ##  ##    ####    ##    ##
        ##          ##  ##      ##########      ##
        ##############  ##  ##    ##  ##    ##





$

With the right font and colours, if held just right, modern smartphones can check this QR code works as intended.


💡