How do you call WebAssembly in your projects?

I am calling Rust functions from Elm in the following way:

type alias A
-(encoder)->      Json Value (Elm)
-(port)->         Object     (Javascript)
-(stringify)->    string     (Javascript)
-(wasm-binding)-> &str       (Rust)
-(serde)->        struct A   (Rust)
-(function)->     struct B   (Rust)
-(serde)->        String     (Rust)
-(wasm-binding)-> string     (Javascript)
-(parse)->        Object     (Javascript)
-(port)->         Json Value (Elm)
-(decoder)->      type alias B (Elm)

Here the ports, encoders and decoders are handwritten while the wasm-binding is created by https://rustwasm.github.io/wasm-pack/ . This is a complicated way to get remote procedure calls for Rust functions from Elm, so I was wondering how everyone else is calling wasm from Elm.

Do you call WebAssembly in your Elm project? How does your “pipeline” differ?

2 Likes

I have not called Rust functions in ports yet but I’m interested, so here are a few suggestions that may be helpful or not…

The port is hand written, but it seems it is always the same except for the wasm function called, so this could be an high order parameter. Then you would only have the encoders/decoders handwritten, which is not different from any other bidirectional port, unless you also have to customize the Serde serialization/deserialization steps, which does not seem to be the case.

You could use Json.Encode.encode to directly pass a string to the port, and Json.Decode.decodeString to decode the result. This removes 4 steps, but does not change much in practice.

You could also try to use miniBill/elm-codec to write both the encoder and decoder at once.

Lastly, I’m not sure if this is possible, but maybe you could pass to the port the name of the rust function used, in addition to the encoded object string, and a return value id, so the port could be completely generic (maybe you need also something for the Serde steps?).

Actually I explored a little a generic JavaScript port a few months ago, here is a simplified unidirectional example:

https://ellie-app.com/6r4M65DbvWya1

This is not exactly the same problem as yours, but maybe this can inspire you.

Thanks for the suggestions I’ll definitely explore them. (Unfortunately life got in the way for at least the next three weeks)

1 Like

I’m currently doing a Rust + wasm + Three.js + elm thing. It is a bit of a mess right now but by the end of the week I might have a clearer idea of how to do things.

My use case was doing a simple interface for an RGB-D visual odometry algorithm. Basically an algorithm that you feed with images and it tries to reconstruct a 3D model and the camera trajectory.

The algorithm is coded in Rust and exposed through a wasm api thanks to wasm-bindgen itself exposed as javascript object thanks to wasm-pack. The three exposed struct are WasmTracker, PointCloud and CameraPath, each with very few public methods.

I have made a simplification hypothesis that all these structs are going to be singletons. This enables me to have only 1 ES6 module (Renderer) keeping internally “global” variables with all useful state and thus reducing communication with Elm to a minimum. This ES6 module also contains a custom element instantiated by Elm and that enables Elm -> wasm communication through the custom elements attributes.

Concretely I have the following communication through ports:

port loadDataset : Value -> Cmd msg
port datasetLoaded : (Int -> msg) -> Sub msg
port newKeyFrame : (Int -> msg) -> Sub msg
port track : () -> Cmd msg
port exportObj : () -> Cmd msg

As you can see, very minimal types, so no decoder / encoder needed. And the custom element is populated like this:

Html.node "custom-renderer"
    [ attribute "width" (String.fromFloat width)
    , attribute "height" (String.fromFloat height)
    , attribute "canvas-id" "canvas-kf"
    , attribute "nb-frames" (String.fromInt nb_frames)
    , attribute "current" (String.fromInt s.current)
    ]

With only the current value changes (slider) triggering wasm code to be executed through functions in the ES6 module:

attributeChangedCallback(name, oldValue, newValue) {
	switch (name) {
		case 'current':
			if (newValue === oldValue) break; // Do not accidentally trigger.
			updateCurrentPointCloud(+newValue);
			updateCurrentCameraPoseKf(+newValue);
			updateCurrentKfImage(+newValue);
			break;
	}
}

If you are interested, the demo is available on github with source code. To run it you need a certain kind of dataset. I have one ready here. Beware, it is a 400Mb tar file and once loaded in the app, it will take 900Mb memory (close tab to free memory).

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.