* Click on the top left icon to toggle darkmode

WASM using c3


First Steps with C3 and Raylib on WebAssembly

Hi! This is my first blog post on this website, and hopefully, it’s the first of many, as I have some free time at the moment.

Today, I spent a few hours experimenting with the programming language C3 and linking it with a static file from Raylib, a popular library for creating web applications. My goal was to build something interactive and deploy it to the web with minimal hassle.

I’ve set up a repository to link C3 with Raylib and stress-test it using a bunny-rendering benchmark, inspired by this bunnymark benchmark.

Here are some key things to keep in mind when compiling C3 and Raylib to WebAssembly (WASM):

  1. Limited WASM Support in C3:
    C3 has limited support for compiling to WASM. The standard library isn’t usable in WASM, so any standard functions need to be rewritten or excluded. For example, I attempted to randomly assign colors to a rabbit, but there was no compile-time error, so I had to debug why WASM wasn’t loading properly. In hindsight, I should have known, given that the --link-libc=no flag was used in the examples.

    Example command:

    c3c compile --reloc=none --target wasm32 -g0 --link-libc=no --no-entry main.c3 raylib.c3
    
  2. Custom Raylib JavaScript File:
    Raylib APIs need to be called through a custom raylib.js file. As shown in the command above, there’s no static Raylib file (raylib.a) linked directly to WASM. Instead, Raylib APIs are accessed through JavaScript using WebAssembly.instantiateStreaming. For example, raylib::init_window would be called in JavaScript like this:

    function InitWindow(width, height, title_ptr) {
      this.ctx.canvas.width = width;
      this.ctx.canvas.height = height;
      const buffer = this.wasm.instance.exports.memory.buffer;
      document.title = cstr_by_ptr(buffer, title_ptr);
    }
    
    const raylibObject = {
      raylib: { InitWindow: InitWindow },
    };
    
    WebAssembly.instantiateStreaming(fetch("main.wasm"), raylibObject).then(
      (obj) => obj.instance.exports.exported_func(),
    );
    

    To actaully link it together, I was lucky to find raylib.js this repo (Thanks tsoding!!). I only needed to add a few functions for handling clicks. The file had RaylibJs class which need to be turned into object from above example and used Proxy class to do that. I actaully never seen that API being used outside of this.

    function make_environment(env) {
      return new Proxy(env, {
        get(_target, prop, _receiver) {
          if (env[prop] !== undefined) {
            return env[prop].bind(env);
          }
          return (...args) => {
            throw new Error(`NOT IMPLEMENTED: ${prop} ${args}`);
          };
        },
      });
    }
    
    class RaylibJs {
       // will have all raylib functions 
       ...
    
       IsMouseButtonPressed(key) {
         return this.currentIsMouseButtonPressed == key;
       }
    
       // entrypoints to fetch wasm and start it.
       start(wasmPath, canvasId) {
         ...
    
         this.wasm = await WebAssembly.instantiateStreaming(fetch(wasmPath), {
           env: make_environment(this),
         });
    
         // Call the main functions from wasm object
         this.wasm.instance.exports.main();
       }
     }
    

Overall, this was an interesting project to spend a few hours on, and maybe in the future, I’ll explore compiling the C3 standard library to WASM.

Below are the results!