DEV Community

loading...
Cover image for WebAssembly, Wasmer And Embedding Wasmer in C program

WebAssembly, Wasmer And Embedding Wasmer in C program

satrobit profile image Amir Keshavarz Updated on ・6 min read

Introduction

In this article, We're going to talk about WebAssembly, Wasmer and how to embed Wasmer in a C application.

WebAssembly

WebAssembly is a portable binary instruction format for virtual stack machines.

It aims to execute at native or near-native speed by assuming a few characteristics about the execution environment.

WebAssembly is designed to be executed in a fully sandboxed environment with linear memory. The binary is completely blind to the host environment by default because of that design. That being said, we can set up some kind of communication by reading memory or importing functions which we'll talk about it later.

Wasmer

We talked about WebAssembly and learned what is all about but this article will be focusing on how to use WebAssembly in action.

Between all of these compilers, interpreter, and runtimes, We'll be covering a runtime named Wasmer which supports multiple backends including LLVM, Cranelift, and Single pass (dynasm).

Backends

  1. Single Pass: super fast compilation, non-optimized execution
  2. Cranelift: normal compilation, normal execution (default)
  3. LLVM: slow compilation, fast execution

Wasmer Basics

Wasmer is crazy easy to install and use so don't worry about its learning curve because It is very simple despite being very flexible and powerful.

Standalone Runtime

Wasmer provides a CLI in order to easily execute wasm files in the command line.

On Linux you can install it like this:

curl https://get.wasmer.io -sSfL | sh

On windows, you can go the Github Releases Page, and there you can find .exe installers.

To check if everything is working fine you can download the qjs.wasm file at the wapm website.

Now you can run Wasmer to get a QuickJS prompt which you can use to run javascript!

wasmer qjs.wasm

Wasmer QuickJS
Crazy, right?

WAPM - Package Manager for WebAssembly

WAPM is a package manager for WebAssembly modules which comes bundled with the Wasmer itself.

In the previous section, we downloaded QuickJS manually but using wapm we can install the QuickJS package much easier.

We can use wapm to install QuickJS globally.

wapm install -g quickjs

And now you can use QuickJS everywhere.

# Run a file
qjs --dir=. examples/hello_module.js

# Run the REPL
qjs

The good thing about WAPM is that it doesn't force you to use Wasmer.

Wasmer Integration in C

Wasmer provides official integration tools for several languages including C/C++, Go, Rust, Javascript, PHP, and C#. Sky's the limit when C headers are provided :)

Embedding Wasmer in C using the already provided header file is pretty straight forward and by following these steps, You should be able to get how things work.

Setup Environment

To embed Wasmer in your C application you'll need a C compiler and the Wasmer C API SDK which you can download from the Wasmer releases page.

  • Linux: wasmer-c-api-linux-amd64.tar.gz
  • macOS: wasmer-c-api-darwin-amd64.tar.gz
  • Windows: wasmer-c-api-windows.tar.gz

After downloading the SDK you need to extract the contents and set the WASMER_C_API env variable to the path which you extracted the SDK in.

# Extract the contents to a dir
mkdir wasmer-c-api
tar -C wasmer-c-api -zxvf wasmer-c-api*.tar.gz

export WASMER_C_API=`pwd`/wasmer-c-api

# Update LD_LIBRARY_PATH to link against the libwasmer.so in the examples
export LD_LIBRARY_PATH=`pwd`/wasmer-c-api/lib/:$LD_LIBRARY_PATH

You can also build the SDK youself. Click here for more information.

Embedding Wasmer

Before writing any code, You need to have a .wasm file ready. In this example, you can use add.wasm which exports a function named add_one. This function takes an int and returns the number plus 1!

You can find the source here.

Now let's get to coding! The first thing you should do is including the header file.

#include <stdio.h>
#include "wasmer.h" // Wasmer C API

Make a main() function and read the add.wasm into the memory.

FILE *file = fopen("add.wasm", "r");
assert(file != NULL);
fseek(file, 0, SEEK_END);
long len = ftell(file);
uint8_t *bytes = malloc(len);
fseek(file, 0, SEEK_SET);
fread(bytes, 1, len, file);
fclose(file);

By calling wasmer_instantiate, You can instantiate WebAssembly Instance from wasm bytes. This is where you can import host functions or memory but importing won't be covered in this article so We just create an empty imports array. (Maybe a part 2 of this article covering advanced topics like importing?)

wasmer_import_t imports[] = {};
wasmer_instance_t *instance = NULL;
wasmer_result_t compile_result = wasmer_instantiate(
   &instance, // Our reference to our Wasm instance 
   bytes, // The bytes of the WebAssembly modules
   len, // The length of the bytes of the WebAssembly module
   imports, // The Imports array the will be used as our importObject
   0 // The number of imports in the imports array
);

wasmer_result_t is an enum that represents a success or a faliure.

  • WASMER_OK: Represents a success.
  • WASMER_ERROR: Represents a failure.

Check the compile_result for its value and exit the program if there's an error.

if (compile_result != WASMER_OK)
{
   return 1;
}

At this point, our wasm file is compiled and we can call the exported functions.

WebAssembly provides 4 value types.

  1. i32 : 32-bit integer.
  2. i64 : 64-bit integer.
  3. f32 : 32-bit floating point.
  4. f64 : 64-bit floating point.

Yes, there are all numbers but by being such a low-level standard there is no way around it.

But don't worry, Strings and even dynamic strings can be easily implanted. Simplest implantation is a C-style string which basically is an array of ASCII bytes with a null byte at the end (hence the name null-terminated string).

You can use wasmer_value_t to define a WebAssembly value. We use it to define 2 values, one for the function parameter and one for the result.

We put them in arrays before calling the add_one function.

wasmer_value_t param_one = { .tag = WASM_I32, .value.I32 = 24 };
wasmer_value_t params[] = { param_one };

wasmer_value_t result_one = { 0 };
wasmer_value_t results[] = {result_one};

wasmer_value_t has two fields.

  1. wasmer_value_tag: WebAssembly type. (WASM_I32, WASM_I64, WASM_F32, WASM_F64)
  2. wasmer_value: Value.

It's time to call the add_one function. This can be done by calling wasmer_instance_call within our C program.

wasmer_result_t call_result = wasmer_instance_call(
   instance, // Our Wasm Instance
   "add_one", // the name of the exported function we want to call on the guest Wasm module
   params, // Our array of parameters
   1, // The number of parameters
   results, // Our array of results
   1 // The number of results
);

Like before we check the call_result to see if the call was successful or not.

if (call_result != WASMER_OK)
{
   return 1;
}

Now we can read the result value which we know it's a 32-bit integer.

int response_value = results[0].value.I32;

Print the result to show the glory :)

printf("Result value:  %d\n", response_value);

We're done here. You should call wasmer_instance_destroy to destroy the instance and return a success exit status code.

wasmer_instance_destroy(instance);
return 0;

This is our final code:

#include <stdio.h>
#include "wasmer.h" // Wasmer C API

int main()
{
    FILE *file = fopen("add.wasm", "r");
    assert(file != NULL);
    fseek(file, 0, SEEK_END);
    long len = ftell(file);
    uint8_t *bytes = malloc(len);
    fseek(file, 0, SEEK_SET);
    fread(bytes, 1, len, file);
    fclose(file);

    wasmer_import_t imports[] = {};
    wasmer_instance_t *instance = NULL;
    wasmer_result_t compile_result = wasmer_instantiate(
        &instance, // Our reference to our Wasm instance
        bytes,     // The bytes of the WebAssembly modules
        len,       // The length of the bytes of the WebAssembly module
        imports,   // The Imports array the will be used as our importObject
        0          // The number of imports in the imports array
    );

    if (compile_result != WASMER_OK)
    {
        return 1;
    }

    wasmer_value_t param_one = {.tag = WASM_I32, .value.I32 = 24};
    wasmer_value_t params[] = {param_one};

    wasmer_value_t result_one = {0};
    wasmer_value_t results[] = {result_one};

    wasmer_result_t call_result = wasmer_instance_call(
        instance,  // Our Wasm Instance
        "add_one", // the name of the exported function we want to call on the guest Wasm module
        params,    // Our array of parameters
        1,         // The number of parameters
        results,   // Our array of results
        1          // The number of results
    );

    if (call_result != WASMER_OK)
    {
        return 1;
    }

    int response_value = results[0].value.I32;

    printf("Result value:  %d\n", response_value);

    wasmer_instance_destroy(instance);
    return 0;
}

For more example, you can use a few wonderful examples provided by Wasmer which you can find here.

Conclusion

In this article we learned about:

  • WebAssembly basics
  • Wasmer standalone runtime and package manager
  • WebAssembly value types
  • Embedding Wasmer in our C application

WebAssembly still is a new concept. There's been lots of cool and yet useful stuff built using WebAssembly. And the interesting thing about WebAssembly is that there is application for it on both client and server sides.

I'm interested to write a part 2 discussing advanced topics like importing functions and reading shared memory and implanting strings and streams but I don't have a plan for it yet.

I hope you enjoyed this article and It didn't waste too much of your time.

Links

  1. https://webassembly.org/docs/portability/#assumptions-for-efficient-execution
  2. https://medium.com/wasmer/a-webassembly-compiler-tale-9ef37aa3b537
  3. https://docs.wasmer.io/
  4. https://wasmerio.github.io/wasmer/c/runtime-c-api/index.html

Discussion (0)

pic
Editor guide