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.