Counter Example

In this example, we will implement a simple Counter struct which has five associated methods: new, increment, decrement, set, and get and one field count.

First create a new project.

$ cargo new counter --bin

Add the following to .cargo\config:

[target.asmjs-unknown-emscripten]
rustflags = [
    "-Clink-args=-s EXPORTED_FUNCTIONS=['_counter_create','_counter_increment','_counter_decrement','_counter_set','_counter_get','_counter_destroy']",
]

Notice we are exporting six functions which correspond to the five methods associated with the Counter struct as well as a counter_destroy function which frees the memory allocated for the Counter struct.

Paste the following code into main.rs:

// The Counter struct.
pub struct Counter {
    count: i32,
}

// Methods on the Counter struct.
impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }

    fn increment(&mut self) {
        self.count += 1;
    }

    fn decrement(&mut self) {
        self.count -= 1;
    }

    fn set(&mut self, count: i32) {
        self.count = count;
    }

    fn get(& self) -> i32 {
        self.count
    }
}

// The C API for using the Counter struct.
#[no_mangle]
pub extern "C" fn counter_create() -> *mut Counter {
    // Allocate a Counter struct on the heap and convert it into a mutable pointer.
    Box::into_raw(Box::new(Counter::new()))
}

#[no_mangle]
pub unsafe extern "C" fn counter_increment(counter: *mut Counter) {
    let counter = &mut *counter;
    counter.increment();
}

#[no_mangle]
pub unsafe extern "C" fn counter_decrement(counter: *mut Counter) {
    let counter = &mut *counter;
    counter.decrement();
}

#[no_mangle]
pub unsafe extern "C" fn counter_set(counter: *mut Counter, count: i32) {
    let counter = &mut *counter;
    counter.set(count);
}

#[no_mangle]
pub unsafe extern "C" fn counter_get(counter: *const Counter) -> i32 {
    let counter = &*counter;
    counter.get()
}

#[no_mangle]
pub unsafe extern "C" fn counter_destroy(counter: *mut Counter) {
    // Convert the mutable pointer back to a box and let it fall out of scope.
    Box::from_raw(counter);
}

fn main() {}

The first part of the code is pretty simple Rust code. We create a new Counter struct and implement the five associated methods for that struct.

The C API portion of the code is slightly more complex. However, it follows a standard pattern discussed in detail here. For the constructor, we Box the Counter struct to allocate its memory on the heap. We then convert the Box into a pointer and return the pointer. We now have a pointer to a Counter struct living on the heap. Any time we want to call a method on a struct, we pass in the pointer, cast the pointer to a reference, and then call methods on that reference as we normally would. Finally, to deconstruct the Counter we convert the pointer back to Box and then let it fall out of scope telling the Rust compiler it can free the memory. Notice, this requires using unsafe blocks.

Thats really it. Now compile it:

$ cargo build --target asmjs-unknown-emscripten

Now lets create some JavaScript code that uses our library. In an index.html file at the top of the project add the following:

<!DOCTYPE HTML>
<html>
<head>
    <script src="target/asmjs-unknown-emscripten/debug/counter.js"></script>
    <script>
        const counter_create = Module.cwrap("counter_create", "number", []);
        const counter_increment = Module.cwrap("counter_increment", "", ["number"]);
        const counter_decrement = Module.cwrap("counter_decrement", "", ["number"]);
        const counter_set = Module.cwrap("counter_set", "", ["number", "number"]);
        const counter_get = Module.cwrap("counter_get", "number", ["number"]);
        const counter_destroy = Module.cwrap("counter_destroy", "", ["number"]);
        const counter_1_pointer = counter_create();
        const counter_2_pointer = counter_create();
        console.log(counter_get(counter_1_pointer));
        console.log(counter_get(counter_2_pointer));
        counter_increment(counter_1_pointer);
        counter_set(counter_2_pointer, 43);
        counter_decrement(counter_2_pointer);
        console.log(counter_get(counter_1_pointer));
        console.log(counter_get(counter_2_pointer));
    </script>
</head>
</html>

You will notice when using Module.cwrap we actually pass a number type where a pointer is expected. There are only three types Module.cwrap can handle (as quoted from the Emscirpten guide Interacting with Code section):

  • number: for a JavaScript number corresponding to a C integer, float, or general pointer
  • string: for a JavaScript string that corresponds to a C char* that represents a string
  • array: for a JavaScript array or typed array that corresponds to a C array; for typed arrays, it must be a Uint8Array or Int8Array

Having to manually mangage pointers in JavaScript is certainly less than ideal. We can easily wrap this functionality in an abstraction to make our asm.js library easier to use. Below is one example using the new es6 class syntax:

const create = Module.cwrap("counter_create", "number", []);
const increment = Module.cwrap("counter_increment", "", ["number"]);
const decrement = Module.cwrap("counter_decrement", "", ["number"]);
const set = Module.cwrap("counter_set", "", ["number", "number"]);
const get = Module.cwrap("counter_get", "number", ["number"]);
const destroy = Module.cwrap("counter_destroy", "", ["number"]);

export class Counter {
    constructor() {
        this.ptr = create();
    }

    increment() {
        increment(this.ptr);
    }

    decrement() {
        decrement(this.ptr);
    }

    set(count) {
        set(this.ptr, count);
    }

    get() {
        return get(this.ptr);
    }

    free() {
        destroy(this.ptr);
    }
}

And using the class looks like the following:

> let c = new Counter();
> c.set(43);
> c.decrement();
> c.get();
> c.free();

This class can be easily used in JavaScript without the need to know the underlying implementation details.

results matching ""

    No results matching ""