Skip to content

"Hello world!" in C via JavaScript

Open in GitHub Codespaces

After passing some numbers to-and-fro between JavaScript and WebAssembly it's time to take things to the next level in two ways: importing host functions in your WebAssembly code and passing more complex data (strings!) back-and-forth with your WebAssembly module. 😵

First, let's see what our C file looks like:

c
__attribute__((
    import_module("env"),
    import_name("print")
)) void print(char* message);

int main() {
    print("Hello world!");
    return 0;
}

😱 We're importing host functionality to print something! The print() function that we are using here must be defined in our JavaScript code that instantiates this WebAssembly module. The fancy __attribute__() stuff here is a Clang-specific trick.

Before we get around to writing our JavaScript code to use the .wasm file we need to compile it. This compilation command is identical to the one used in the Fibonacci sequence in C via JavaScript example:

sh
clang-17 \
  --target=wasm32 \
  -nostdlib \
  -Wl,--no-entry \
  -Wl,--export-all \
  -o hello_world.wasm \
  hello_world.c

Now that we have our hello_world.wasm file lets write some JavaScript to provide it with the print() function and then run it! 🚀

js
// 1. Load the binary contents of the WebAssembly file.
///   If you were in Node.js you'd use 'fs.readFile()'.
const response = await fetch("hello_world.wasm");
const buffer = await response.arrayBuffer();

// 2. Compile the WebAssembly blob into a 'WebAssembly.Module'.
//    This will throw an error if the buffer isn't valid WebAssembly.
const module = await WebAssembly.compile(buffer);

// 4. Define our host-defined 'print()' function that our C code will use. See
//    below for how we convert the C-string pointer into a JavaScript string.
function print(messagePointer) {
  const message = cStringPointerToString(messagePointer);
  console.log(message);
}

// 4. Instantiate the module with any imports. We need to pass our 'print()'
//    function here so that our WebAssembly can use it.
const importObject = {
  env: {
    print: print,
  },
};
const instance = await WebAssembly.instantiate(module, importObject);

// 5. Define a function to convert a C-string pointer into a JavaScript string.
function cStringPointerToString(pointer) {
  // A) First we interpret the raw memory as bytes.
  const { memory } = instance.exports;
  const bytes = new Uint8Array(memory.buffer);

  // B) We scan through the string until we see a '\0' (null) char.
  let length = 0;
  while (true) {
    if (bytes[pointer + length] === 0) {
      break;
    } else {
      length++;
    }
  }

  // C) Then we select just the string's contents.
  const chars = bytes.subarray(pointer, pointer + length);

  // D) And finally we decode those UTF-8 characters into a JavaScript string.
  return new TextDecoder().decode(chars);
}

// 6. Finally actually run the exported 'main()' function which does some logic
//    and calls the 'print()' function that we gave the WebAssembly module.
const { main } = instance.exports;
main();
index.html
html
<script type="module" src="index.js"></script>
<p>Check the DevTools console!</p>

Now we can start up an HTTP server with python -m http.server or your other favorite static HTTP server and see the results:

It's worth noting that this is a demonstration of how WebAssembly modules can pass strings to their higher level hosts. For real-world integrations it's a good idea to check out wasm-pack, Emscripten, the WebAssembly Component Model, or other more complex tooling.