something that just occurred to me: I can't think of a reason why Roc couldn't support a Task-based WebAssembly FFI :thinking:
for example (just making things up here) we could have a module type called wasm and in its header it specifies a .wasm file which it wraps
Like instead of requiring a zig/c/rust shim from js to roc?
basically as a way to take platform-agnostic code that's written in another language and call it from Roc applications without having to get the platform involved, or use dylibs
and share them in packages etc.
there might be security concerns I'm missing though, e.g. around memory access
So is the external library we are wanting compiled and packaged into a WASM library?
yeah
SIMDjson might be an interesting example of that
And then platforms can recieve Tasks from roc saying "load this 'someCbutNowWasm.wasm library, and call X passing Y"
yeah something like that
or just like function pointers
hm, it might be too difficult to validate things though
I don't see why this wouldn't work
like if it says it returns a string, verifying that it's valid UTF-8
or that reference counts were done correctly
If we are just passing standard Roc Types back and forth across the host boundary, and these are translated into types WASM understands
yeah I'm just thinking about what a malicious actor could do in the package ecosystem
Or I guess you could have WASM enabled hosts, that support roc packages which includes WASM binaries
like if it's all .roc files, there are certain exploits that aren't possible, so the question becomes - if there are now .wasm files too, is there some way we can maintain that guarantee?
that you can install and run any roc package and it can't access other parts of the process's memory space, for example
The host/platform still controls everything at that boundary... so unless things can bust out of WASM runtimes I don't see how this could be an issue
^^ that being said, I'm no security expert
Just early thoughts about this
Oh, this is for adding tasks to a package while trying to avoid adding general ffi to roc
yeah
I assume they'd need to be tasks, but maybe that's not a correct assumption either :laughing:
So would enable someone to write some C and compile it to wasm and then call it wasm
right
And I assume this is freestanding, not wasi? So no file io or anything?
kinda - I think something like WIT where the wasm file says "here are the operations I require"
which we could then wrap as module params
and they'd slot in nicely
e.g. if a wasm file says "I need to be able to write to a file" then that operation can be provided in the normal Roc way (module params) just like anything else
If they can do arbitrary system ffi via wasi (even if explicitly specified via wit), why limit to wasm?
hm, I don't understand the question :sweat_smile:
limit to wasm as opposed to what?
Feels like a case where we should just allow wrapping a native dynamic library as tasks without any interaction with the platform.
wasm can't do syscalls
that's the only reason it's a possibility haha
wasm can't do syscalls
That's exactly what wasi is for?
ok I misunderstood earlier then
And wit is for giving access to specific parts of wasi.
I guess I mean "freestanding wasm" and not "freestanding wasi"
like the type of wasm you could run in a browser, where you have to provide it with everything and it doesn't know how to do anything natively
other than basic CPU operations and memory stuff
Ok yeah, that makes more sense. Was very confused by the mention of wit where you can do package wasi:filesystem;.
yeah sorry, I meant something like WIT but not exactly that
The package author includes a WIT file describing the interface for their WASM module, and we may be able to use that to generate the interface on the Roc side?
yeah something like that
Also, yes, would still need to be tasks. Cause wasm can use globals among other things that could be very unsafe in roc.
yeah for sure if it uses stateful things like globals
but I could imagine a scenario where you basically call the wasm function in its own isolated sandbox (e.g. give it its own memory arena and don't let it see anything else) and then don't maintain any state in between invocations
and then we could either do that, or maintain state in between calls, depending on whether you specified to call it as a Task or not
So I would say:
Extending platforms with wasm modules that get called by roc should be doable.
By making the wasm freestanding, it won't have any access to ffi.
Will need to use Task due to being able to hold onto state.
Calling into it will of course have all of the memory copying gripes. That said, memory returned from wasm should be possible to directly reference.
We could even use a wasm interpretter that jits/compiles to native to get really solid perf.
yeah I'd ideally want to compile it all ahead of time
so it just ends up in the binary and there's no wasm runtime in the compiled binary
I don't know of a reason why that wouldn't be possible
I believe binaryren loads .wasm files into LLVM IR in order to run LLVM optimization passes on it and then output another .wasm file
Yeah, should be doable. So during roc compilation time would compile the wasm and setup the memory restrictions and such.
but if that's possible, then it's also possible to load it into LLVM IR and then emit machine instructions
right
load it into LLVM IR and then emit machine instructions
Just leaves an open question if it becomes less safe/has an easier time escaping the interpreter (cause it is compiled away)
but yeah, all sound doable.
I think passing data in should be straightforward theoretically (it all has to be copied, which is unfortunate, but I don't see a way around that)
I guess theoretically it could be possible to do some Morphic-esque analyis of like "this value is only ever going to be passed into wasm" and then do the roc_alloc equivalent directly into the memory wasm will be given access to, but I dunno about that :laughing:
in practice, that is
anyway, so assuming copying bytes in, and then copying bytes back out...
we'd need some way to verify the bytes being copied out
and then copying bytes back out...
This isn't necessary
Or at least shouldn't be
it is if we're maintaining state, right?
otherwise the next time you call into wasm it could have stored a pointer into what it gave back last time
Oh, to stop wasm from storing a version of a list and then mutating it in place.
and then modify some distant part of the program
yeah
.wit "world" inside a roc package describes the interface for the bundled WASM binary
default world simple_world {
import {
// Importing an addition function from the host environment
fn add(a: i32, b: i32) -> i32
}
export {
// Exporting a multiplication function from the WASM module
fn multiply(a: i32, b: i32) -> i32
}
}
The roc uses this WIT file to generate and provide Task based interface
module SimplePackage {
# this is a module parameter that is required to instantiate this package
# I'm not sure if we have types in the syntax for module params
add : { a : I32, b : I32 } -> Task I32 *
} [
multiply,
]
multiply : { a : I32, b : I32 } -> Task I32 *
This feels so defensive, but I get the goals.
Personally, if a user opts into it, I would prefer to just allow roc to load a shared library as a platform extension.
It can be unsafe, cause it is at the platform level.
Any, but yeah for wasm that all sounds good. Lots of copies, but otherwise just a task based interface and it should be fine.
Actually, only fine if we recursively copy everything in and out.
Like a list of strings would need to copy the list and every string into and out of wasm
right
To guarantee wasm dosen't do anything evil
so a potentially very valuable use of this could be math functions that don't do heap things anyway
like is there a BLAS/LAPACK compiled to wasm anywhere? :thinking:
eh, most math functins that matter are for multidimensional arrays. So lots of data
hm, large ones?
I mean I guess for some game programming stuff it would help. But for most stuff blas is used for, they tends to be at least medium sized. So all the copies could really hurt.
Like I don't think it would work for a generic blas wrapper. But it would probably work if you made a full blas simulation function with many operations and exposed it as one effect.
interesting
Why do we need to copy the data? The host is still in control of the information which is sandboxed inside the WASM runtime
I think the copies would get too expensive if you have 2 copies for every single matrix add, multiply, etc.
Why do we need to copy the data?
Have to copy in to allow wasm to see it cause it is sandboxed
Have to copy out to stop wasm from holding a reference and mutating it later such that roc seeing random changes to data that is supposed to be constant.
Can you allocate the RocList into an arena, pass that into WASM to modify and then when WASM returns you know it cannot do anything more so it's safe to pass back to roc
you know it cannot do anything more
This is only true if wasm has not state from call to call
Maybe we can restrict the wasm to no globals?
I don't know enough about wasm's memory model to be sure if this would work, but maybe in the wasm interop wrapper you could opt into some restrictions that gain performance without sacrificing security, specifically:
List etc.TaskThen only pure functions would exist.
Yeah, I think wasm without globals and some extra checks could go a long way. Still definitely wouldn't be safe, but we could at a minimum just do a copy for uniqueness before handing off to wasm.
yeah a relevant question is - given the security requirements, what use cases are left that would be useful in practice?
I guess a possible answer in general is "a thing that at least works, and then in the future it can be rewritten in Roc to be faster because it doesn't have the security overhead"
Maybe if you have a big function that is written in C or something and it's been verified or is trusted and you don't want to rewrite it.
I think the real issue is that it likely would be hard to use existing libraries (especially if no globals/state). So you would be writing raw c/rust/zig for the wasm. Definitely could be used to speed up some computations, but a much bigger lift to create a library for it. Probably can't just import blas/lapack/eigen/tf/etc and build for wasm with no globals and a thin type shim for roc lists.
WASM is already pretty restricted crossing the host boundary. So I wonder if the copy in and out is really that bad?
So I would label it as a potential gain, but personally, I would turn to raw ffi in a platform with a shared library calling into blas before I would use something like this. But I definitely could be really biased.
Noting the massive potential boost to the ecosystem from being able to use code that is written in any language that compiles to WASM
So I wonder if the copy in and out is really that bad?
Really depends on use case and how small of a chunk of code each function is. As I mentioned above, calling into a large function that will take a lot of time anyway is probably fine. Calling into wasm for individual ops probably is too costly.
And I think for this to be really nice, you would want to call into it for each individual op.
for each individual op.
What do you mean by this? ... as in each function call?
Like I would want to expose the roc-wasm-matrix library that has all of the matrix operation super fast in wasm. Then the end user can make individual calls to add and sub and matmul. But that would be 2 copies for every matrix add.
But you could now have a Task to load data into WASM, and then the calls could be instructions to operate on that data, and another to eventually get the data back out right?
So you only need to have Two expensive copies, one in and one out
So wasm returns a handle back to roc and roc works with the handle until it needs the data back out.
So basically, treat it like we treat file today.
So 100% has to be task cause we have to allow wasm to hold state, but as long as we delay returning state, it should be safe from scary mutation and mostly copy free.
Yeah, that sounds doable.
x = WasmMatrix.createMatrix! someNumList [12, 22]
y = WasmMatrix.createMatrix! someNumList2 [22, 36]
x = WasmMatrix.mulF32! x 7.2
x = WasmMatrix.subInt! x 3
z = WasmMatrix.matmul! x y
out = WasmMatrix.extractMatrix z
...
Not exactly sure how x and y will get freed, but sounds feasible.
Also, probably still will really confuse users depending on if it is inplace or not:
x = WasmMatrix.createMatrix! someNumList [12, 22]
x2 = WasmMatrix.mulF32! x 7.2
Is x equal to x2?
Richard Feldman said:
but if that's possible, then it's also possible to load it into LLVM IR and then emit machine instructions
I might be misunderstanding here. But the mental model I had in my head, is that on the Roc side it's just Tasks and an abstract/opaque interface.
The host exposes some standard set of calls to roc to work with WASM modules, and roc uses those to instruct the host what to call and what arguments to provide etc.
So the package WASM binary is dynamically linked/loaded by the host. It can be cached in .cache/roc along with the other .roc files when roc runing an app.. or for building a executable it is expected to be available in a sub directory like /wasm-packages or in a path from an environment variable at runtime.
The idea is that roc will compile it into the binary at roc app compilation time. Nothing done on the platform side.
Oh interesting...
So the host may never even know
A package would include a task module and a .wasm file. Roc would hopefully compile the wasm to sandboxed native code. Could also just embedded the entire .wasm file into the binary with a wasm interpreter as well.
Lol, imagine a roc app compiled to WASM, that is using a WASM package that is running inside a WASM interpreter
And all of this is running inside a WASM interpreter
presumably if we know that the compilation target is wasm, we could make use of that knowledge to avoid silliness :big_smile:
I wonder how transitive dependencies would be handled? Like, we would only need to have one WASM binary per package, and the whole app will only use one version for each package.
Last updated: Jun 16 2026 at 16:19 UTC