I'm trying to get Roc to generate a .wasm
file. I've been following the instructions in examples/platform-switching/web-assembly-platform/README.md
but I can't seem to get anything to work. I'm not really sure where I'm going wrong.
I would love to be able to generate a valid WASM file, and potentially even something that could be used with WASI. I appreciate that there probably aren't many platforms yet, and we don't have RocGlue for it, but I'm interested in working on this.
I've thought it might be fun to try and make something work for WCGI. But don't have much experience, and I'm thinking it may be too big a project for me but worth giving it a go.
$ cargo run -- build --target wasm32 examples/platform-switching/rocLovesWebAssembly.roc
Compiling palette v0.6.1
Compiling signal-hook v0.3.15
Compiling pulldown-cmark v0.9.2
Compiling roc_cli v0.0.1 (/Users/luke/Documents/GitHub/roc/crates/cli)
Compiling roc_repl_expect v0.0.1 (/Users/luke/Documents/GitHub/roc/crates/repl_expect)
Compiling roc_code_markup v0.0.1 (/Users/luke/Documents/GitHub/roc/crates/code_markup)
Compiling roc_docs v0.0.1 (/Users/luke/Documents/GitHub/roc/crates/docs)
Finished dev [unoptimized + debuginfo] target(s) in 8.27s
Running `target/debug/roc build --target wasm32 examples/platform-switching/rocLovesWebAssembly.roc`
🔨 Rebuilding platform...
An internal compiler expectation was broken.
This is definitely a compiler bug.
Please file an issue here: https://github.com/roc-lang/roc/issues/new/choose
thread '<unnamed>' panicked at 'Error:
Failed to rebuild host.zig:
The executed command was:
zig build-obj examples/platform-switching/web-assembly-platform/host.zig -femit-llvm-ir=examples/platform-switching/web-assembly-platform/main.bc --pkg-begin str crates/compiler/builtins/bitcode/src/str.zig --pkg-end --library c -target i386-linux-musl -fPIC --strip
stderr of that command:
./examples/platform-switching/web-assembly-platform/host.zig:7:9: error: This platform is for WebAssembly only. You need to pass `--target wasm32` to the Roc compiler.
@compileError("This platform is for WebAssembly only. You need to pass `--target wasm32` to the Roc compiler.");
^
', crates/compiler/build/src/link.rs:1493:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' panicked at 'Failed to (re)build platform.: Any { .. }', crates/compiler/build/src/program.rs:941:46
I think I might be using the wrong version of zig. Just going to build 0.9.1 from source and see if that helps. didn't help
It seems to think the target is i386-linux-musl
. Using --target wasm32
doesn't seem to do anything. Indeed when I hardcode it to Target::Wasm32
in crates/cli/src/main.rs
this doesn't change the arguments to zig build.
So I searched for where that might be and have found this in crates/compiler/build/src/link.rs
which I can hardcode to wasm32-wasi
but then I get a successful build with a series of errors like below when it tries to link with ld-temp.o
.
let zig_target = if matches!(opt_level, OptLevel::Development) {
"wasm32-wasi"
} else {
// For LLVM backend wasm we are emitting a .bc file anyway so this target is OK
"i386-linux-musl"
// ^^^^^^^^^^^^^ If I change this to "wasm32-wasi" I get a sucessful build the below errors.
};
wasm-ld: warning: Linking two modules of different target triples: '/Users/luke/
Documents/GitHub/roc/target/debug/build/wasi_libc_sys-5e2583e767e30111/out/wasi-
libc.a/Users/luke/Documents/GitHub/roc/target/debug/build/wasi_libc_sys-5e2583e7
67e30111/out/zig-cache/o/4811dec3fd38e36f9330ff74e399d7a1/memset.o' is
'wasm32-unknown-wasi-musl' whereas 'ld-temp.o' is 'wasm32-unknown-unknown-unknown'
Ok, so I've realised that I have this working now. They are just compiler warnings that I have above. Only realised after manually compiling the host using zig. I had to fix a minor bug in host.zig
to get it to compile. Modified some things... and now I have a roc platform that runs with wasmer.
% wasmer run examples/platform-switching/rocLovesWebAssembly.wasm
Hello, from Zig!
Is it possible to target wasm
using a Rust platform at the moment?
I'm not sure of the current status. I do remember at some point in the past the compiler's build system only supported Zig platforms. That goes back to our first Wasm examples with llvm. I'm not sure if that restriction has been lifted since then. I did a lot of the Wasm support but not this part. @Folkert de Vries might know more about it.
We'd need a specific Rust platform to work on. I don't think one exists right now.
Also note that Roc glue exists at the language level (Rust/Zig), not the instruction set level (x86_64/Wasm).
Rust glue does support the Wasm target as far as I know
Super. Looking forward to building a rust platform for wasm with glue. I might need a minimal example and then I should be good to add more features. I think Brendan is working on an example using glue, not sure if he's thinking wasm though.
we do try to generate glue for wasm, see
#[cfg(target_arch = "arm")]
mod arm;
#[cfg(target_arch = "arm")]
pub use arm::*;
#[cfg(target_arch = "aarch64")]
mod aarch64;
#[cfg(target_arch = "aarch64")]
pub use aarch64::*;
#[cfg(target_arch = "wasm32")]
mod wasm32;
#[cfg(target_arch = "wasm32")]
pub use wasm32::*;
#[cfg(target_arch = "x86")]
mod x86;
#[cfg(target_arch = "x86")]
pub use x86::*;
#[cfg(target_arch = "x86_64")]
mod x86_64;
#[cfg(target_arch = "x86_64")]
pub use x86_64::*;
but I don't think that's been tested in any way so far
Status: Blocked - I think I need assistance, still trying to figure it out.
Get a minimal example that compiles a Roc platform/app to a WASM file for e.g. a Netlify serverless function. Basically I just want to use STDIO and STDOUT for request/response, and make an Echo example.
Basic concept is something like mainForHost : List U8 -> Str
or similar, with a platform lib.rs
maybe something like this.
use std::io::{self, Read};
fn main() {
let mut buf = Vec::new();
io::stdin().read_to_end(&mut buf).unwrap();
let mut roc_str = RocStr::from_slice_unchecked(&mut buf);
unsafe { roc_main(&mut roc_str) };
io::stdout().write_all(roc_str.as_bytes()).unwrap();
io::stdout().flush().unwrap();
}
Using examples/platform-switching/rust-platform
as a starting point I get.
% roc build --target wasm32 --no-link rocLovesRust.roc
0 errors and 0 warnings found in 36 ms while successfully building:
rocLovesRust.o
% wasm-ld --no-entry --export-dynamic --allow-undefined -o rocLovesRust.wasm rocLovesRust.o
% wasmer rocLovesRust.wasm
error: failed to run `rocLovesRust.wasm`
╰─▶ 1: Error while importing "env"."__udivti3": unknown import. Expected Function(FunctionType { params: [I32, I64, I64, I64, I64], results: [] })
When I ask ChatGPT how to fix it I get this.
The error you're encountering indicates that the WebAssembly module you generated is trying to import a function named __udivti3 from the "env" namespace, but it can't find it. The __udivti3 function is a compiler-generated helper function for 128-bit unsigned integer division. It is part of the compiler-rt library, which provides runtime support for various compiler-generated functions. To resolve this issue, you need to link your Rust code with the appropriate wasm32 version of the compiler-rt library.
And this is where I get stuck.
Any ideas on how to get rust to link compiler-rt library in would be most appreciated.
I tried another method and created a rust project for the sole purpose of compiling the missing functions into .o
files against the wasm32-unknown-unknown
target, and then tried linking them using wasm-ld
, but it still doesn't seem to work. I really don't have much experience here, so this is probably a crazy idea. Sharing in case this helps.
% wasm-ld --no-entry --export-dynamic -o rocLovesRust.wasm rocLovesRust.o ./udivmodti4.o ./udivti3.o ./libcompiler-rt.a
wasm-ld: error: lto.tmp: undefined symbol: __udivti3
wasm-ld: error: lto.tmp: undefined symbol: __udivti3
wasm-ld: error: lto.tmp: undefined symbol: __multi3
I've pivoted to just use the Zig platform. It works really well. I hope to polish it a little and hopefully have a working example for the next meetup. Just a few (maybe just one) compiler issues to fix so this can be used by anyone using a nightly release and a URL platform. :smiley:
The following builds WASM ok with roc build --target wasm32 examples/echo.roc
, and also runs well using % wasmer run examples/echo.wasm
, except it prints out hello, world!%
instead of what I expect it to which is Goodbye!%
. I'm pretty confident it is a combination of my lack of Zig, and not quite understanding how we call the exposed Roc functions.
Looking for any pointers to get Roc to take the list of bytes in and return the modified list back.
platform "serverless"
requires {} { main : List U8 -> List U8 }
exposes []
packages {}
imports []
provides [mainForHost]
mainForHost : List U8 -> List U8
mainForHost = main
// <-- other Roc related definitions above here removed for brevity -->
pub extern fn roc__mainForHost_1_exposed_generic(ret: *RocList) void;
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const helloStr = "hello, world!";
var str = RocList.fromSlice(u8, helloStr[0..]);
roc__mainForHost_1_exposed_generic(&str);
const maybeBytes = str.elements(u8);
const bytes = maybeBytes orelse {
std.debug.print("Error getting elements\n", .{});
std.process.exit(1);
};
const array_len: usize = str.len();
const byteSlice = bytes[0..array_len];
try stdout.writeAll(byteSlice); // Write the slice to stdout
}
app "echo"
packages { pf: "../platform/main.roc" }
imports []
provides [main] to pf
main = \_ ->
Str.toUtf8 "Goodbye!\n"
the signature of pub extern fn roc__mainForHost_1_exposed_generic(ret: *RocList) void;
needs two arguments
first the input string, and then a pointer to store the output into
Thank you. I thought that might be the case, but then I get the following error which I have trouble understanding or debugging. There is only one place were I define roc__mainForHost_1_exposed_generic
so I don't think that is causing it.
wasm-ld: warning: function signature mismatch: roc__mainForHost_1_exposed_generic
>>> defined as (i32, i32) -> void in /var/folders/48/39th9k0n0wdcj18k3yhm_g5c0000gn/T/roc_appNrHux0.o
>>> defined as (i32) -> void in lto.tmp
0 errors and 0 warnings found in 427 ms while successfully building:
examples/echo.wasm
luke@192-168-1-104 roc-serverless % wasmer run examples/echo.wasm
error: failed to run `examples/echo.wasm`
│ 1: RuntimeError: unreachable
╰─▶ 2: unreachable
pub extern fn roc__mainForHost_1_exposed_generic(arg: *RocList, ret: *RocList) void;
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const helloStr = "hello, world!";
var arg = RocList.fromSlice(u8, helloStr[0..]);
var ret = RocList.empty();
roc__mainForHost_1_exposed_generic(&arg, &ret);
const maybeBytes = ret.elements(u8);
const bytes = maybeBytes orelse {
std.debug.print("Error getting elements\n", .{});
std.process.exit(1);
};
const array_len: usize = ret.len();
const byteSlice = bytes[0..array_len];
try stdout.writeAll(byteSlice); // Write the slice to stdout
}
Specifically, any idea where the defined as (i32) -> void in lto.tmp
might be referring to?
ah, ok
that signature is just wrong then. i'm not sure how the wasm backend generates them
you might have more luck if you try to build the roc app with --optimize
. that ll use the llvm wasm backend
just to figure out if that is the issue
Unfortunately, that didn't change anything. I still get the same error with --optimize
:sad:
Is your code pushed to a branch or repo somewhere? Would be nice to be able to look at and play with it.
Ok, I'll do that. Just out for a few minutes.
Here is the platform roc-serverless, it doesn't build as is due to the zig builtins issue we've been talking about. I have it working with a local modification in my roc cli so that it knows where to find the builtins. I pushed an update with the builtins locally, so it does build and reproduce the WASM error above.
To me this is using the LLVM backend. That is the default, you don't have to specify --optimize
. Rather, you need --dev
to get the dev one. And the dev backend does not use wasm-ld
, it has its own built-in linker.
At a guess, lto.tmp sounds like a temporary file used for link time optimization by wasm-ld. Again, I'm guessing!
No idea how it would have two different signatures though
Maybe try it with --dev
and see what happens!
I forgot I had another change I had to make in the compiler to get this to run. Added to README.
I tried with --dev
but that still gives the the same function type mismatch. Btw this was using % cargo run -- build --target wasm32 --dev ../roc-serverless/examples/echo.roc
to build it. It gives
<-- other warnings removed -->
wasm-ld: warning: Linking two modules of different target triples: '/Users/luke/Documents/GitHub/roc/target/debug/build/wasi_libc_sys-5e2583e767e30111/out/wasi-libc.a/Users/luke/Documents/GitHub/roc/target/debug/build/wasi_libc_sys-5e2583e767e30111/out/zig-cache/o/ea7f435e8379892b4b98d80660f9fbc9/memset.o' is 'wasm32-unknown-wasi-musl' whereas 'ld-temp.o' is 'wasm32-unknown-unknown-unknown'
wasm-ld: warning: function signature mismatch: roc__mainForHost_1_exposed_generic
>>> defined as (i32, i32) -> void in /var/folders/48/39th9k0n0wdcj18k3yhm_g5c0000gn/T/roc_appf32a4I.o
>>> defined as (i32) -> void in lto.tmp
Btw the other change is
First, change
"i386-linux-musl"
to"wasm32-wasi"
on line 373 incrates/compiler/build/src/link.rs
Which leads me to wonder if there is an issue in cli where it doesn't recognise --dev
when we have --target wasm32
How can I confirm if it is using the dev backend and not LLVM?
Based on the source code here, it looks like wasm does ignore the --dev
flag.
Though it also looks like it would always use the dev wasm and never llvm
So definitely a bit confused by that
I think I found it, if I change BuildConfig::BuildAndRunIfNoErrors
to BuildConfig::BuildOnly
in cli/src/lib.rs then it builds correctly using the dev backend, and not wasm-ld
Actually, I think that just changes the linking_strategy from LinkingStrategy::Legacy
to LinkingStrategy::Additive
which builds without errors, but then there is an issue running the wasm, which I think may because it doesn't actually add the app into it or something.
Interesting. I think this is the issue. We always call llvm when the backend is set to wasm: https://github.com/roc-lang/roc/blob/ffe30af2167385ad75cb89c073608166df18b0e7/crates/compiler/build/src/program.rs#L116-L124
Changing it to works;
CodeGenBackend::Wasm => {
gen_from_mono_module_dev_wasm32(arena, loaded, preprocessed_host_path, wasm_dev_stack_bytes)
}
Yeah, that actually makes sense, cause other stuff is not expecting the development backend and is using the wrong file becuase it is matching what the llvm backend wants for files.
You also need to update this, which is another bug/discrepency: https://github.com/roc-lang/roc/blob/ffe30af2167385ad75cb89c073608166df18b0e7/crates/cli/src/lib.rs#L606-L616
to:
match (
matches.is_present(FLAG_OPTIMIZE),
matches.is_present(FLAG_OPT_SIZE),
matches.is_present(FLAG_DEV),
) {
(true, false, false) => OptLevel::Optimize,
(false, true, false) => OptLevel::Size,
(false, false, true) => OptLevel::Development,
(false, false, false) => OptLevel::Normal,
_ => user_error!("build can be only one of `--dev`, `--optimize`, or `--opt-size`"),
}
Then with --dev
, it seems to compile fine.
Not sure if it works though
So yeah, some of the wasm pipelining seems to have been messed up over time.
Not sure what is breaking with the llvm backend though. That is a separate issue.
So that gives me a .o
file instead of a .wasm
file now.
% cargo run -- build --target wasm32 --dev ../roc-serverless/examples/echo.roc
🔨 Rebuilding platform...
0 errors and 0 warnings found in 394 ms while successfully building:
../roc-serverless/examples/echo.o
I'm not really sure, but fundamentally, we need to rework these pipelines.
Also, I think we need to make two different targets for wasm32
, one for browser, and one for wasi.
Also, echo.o
may just be a misnaming of echo.wasm
. Assuming the wasm dev backend generated correctly, I think it was just filling in whatever file name it was given.
Yeah, unfortunately that doesn't seem to be the case.
% wasmer validate examples/echo.o
error: failed to validate `examples/echo.o`
╰─▶ 1: Validation error: unexpected end-of-file (at offset 0x0)
haha. ok
So if I comment out the wasm_module.eliminate_dead_code(env.arena, called_fns);
in build_app_binary
in crates/compiler/gen_wasm/src/lib.rs
is does generate a valid wasm filed, named echo.o
. But then I have this issue;
% wasmer validate examples/echo.o
Validation passed for `examples/echo.o`.
% wasmer run examples/echo.o
error: failed to run `examples/echo.o`
│ 1: failed to instantiate WASI module
│ 2: Instantiation failed
╰─▶ 3: Error while importing "env"."roc_panic": unknown import. Expected Function(FunctionType { params: [I32, I32], results: [] })
So I think I'm almost there, it looks good in the wasm module. I just don't have a zig implementation for roc_panic
I think. I'll see if I can figure that out.
Adding roc_panic
like this, and now I have a different issue.
export fn roc_panic(_: *anyopaque, _: *anyopaque) void {
// do something?
}
% wasmer run examples/echo.o
error: failed to run `examples/echo.o`
│ 1: failed to instantiate WASI module
│ 2: Instantiation failed
╰─▶ 3: Error while importing "env"."roc__mainForHost_1_exposed_generic": unknown import. Expected Function(FunctionType { params: [I32, I32], results: [] })
Can you switch away from the generic version of the function and to the concrete one
Fundamentally should be something like roc__mainForHost_1_exposed(arg: *RocList) *RocList
Maybe dev wasm doesn't generate the generic version...not sure though
Hmmm, not sure about that.
...
pub extern fn roc__mainForHost_1_exposed(arg: *RocList) *RocList;
pub fn main() !void {
...
var arg = RocList.fromSlice(u8, helloStr[0..]);
var ret: *RocList = roc__mainForHost_1_exposed(&arg);
% wasmer run examples/echo.o
error: failed to run `examples/echo.o`
╰─▶ 1: RuntimeError: Parameters of type [] did not match signature [F64, I64, I64] -> [F64]
It might have worked actually, the issue now might be my main.
Exports:
Functions:
"_start": [F64, I64, I64] -> [F64]
Awesome! Though i have no ideas for that error.
Yeah, trying to figure this one out is tricky. I think we've got the right code for working with Roc, and my issue is purely WASI specific now. Start should be "_start": [] -> []
Oh that's right I never implemented that for the generic main. Never had an example of it.
Ok, I think I've exhausted up to the limit of my knowledge for now.
I've reverted back to the simpler main : Str
for the platform (as in the rocLovesWasm example) and want to focus on integrating with a cloud. I've uncovered a some issues here, and I'm not confident enough to say which of these need to be fixed or investigated further. I'm happy to log issues for anything I've mentioned above we want to keep track of, just need a pointer here and there.
For Rust Host -> WASM it seems the main issue is that we only support generating WASM when we have a host.zig
host. All the Rust examples have a host.c
host. I have an example Rust platform that I've added a host.zig
to. I'm reasonably confident it should work, cargo check
is happy etc, and it generates valid wasm, however it doesn't seem to build and then link in any of the Rust (and by extension the Roc) functions into the end file.
For Zig Host -> WASM there are a few issues in the build tooling which I've discussed above. If these issues are worked around with manual compiler patches, we end up with _start
not being generated correctly which is the standard entry for WASI. Brendan suggested we need a separate target for WASI; I feel like a Roc app will always have a main so we can always support WASI without issues.
I've pushed the latest I have to github and updated README to work using current main, in case anyone would like to see.
I'll make a few comments that some people might know already but others might not.
The _start
function is not unique to WASI or Wasm, native binaries have the same thing. When you write a C program with a int main(int argc, char** argv)
, the C compiler inserts code to call main
from a _start
function that is usually written in assembly and provided by libc
. It sets up the stack and the values of argc
and argv
and then calls main
.
https://embeddedartistry.com/blog/2019/04/08/a-general-overview-of-what-happens-before-main/
The Roc entry point main
is of course not the overall entry point for the whole binary. The real main
is in Zig or Rust or whatever, and that is the one that should be called from _start
. _start
normally has no arguments. I can't remember if it returns an exit code as i32, or if it returns void.
In the virtual DOM platform that I started work on a while ago (and is now on hold) the client side Zig platform has no main
or _start
because we are running in a browser, not an OS, and there are no command line arguments etc.
Similarly if Roc is being used to generate a plugin for a game engine or something, we will compile to an object file or dynamic library rather than an executable, and it will have no main
.
do we actually need wasm-ld
anymore?
now that we have a surgical linking implementation
Looking at the current code, wasm-ld
is used in a few places, all driven by zig. It is used for compiling the host, preprocessing the host, and linking if not using the additive linker.
gotcha - but if we switched to always use the additive linker for compiling Roc application code, then it would only be used on the host and that's it?
Yeah, I think so. Would only need it for building and preprocessing the host. That said, no idea how it works and what it would take to use with the llvm backend. No idea if any part of it is special.
Not sure what you mean by special, but can't think of anything unusual about it.
But as I mentioned in another thread recently, the surgical / additive linking is quite specific to the Dev Wasm backend. It is not a general purpose linker like wasm-ld is. It could be developed into something more but why? Wasm ld does what we need.
We need it to create a preprocessed host
Also my linking code will not work for llvm
well ultimately the goal in general (not just for wasm, but all backends) is that:
roc
is the only thing you need to download to use Roc, which is a goal, and also so we can complete linking as fast as possible, which is also a goal!)Ok so if we always start from a preprocessed host then a platform author needs wasm-ld to create that.
oh yeah platform authors will totally need third-party tools to create preprocessed hosts :big_smile:
but that's already unavoidable because they need some other language besides Roc to create the host anyway
so really I should have said application authors shouldn't need anything other than roc
to use it
Richard Feldman said:
do we actually need
wasm-ld
anymore?
Ok so back to this question, the answer is that the llvm backend needs it because our surgical linking doesn't work with that.
And we also need it to build any of our official platforms or example platforms.
I got an example working on NodeJS via wasm - https://github.com/roc-lang/roc/pull/5346 - but I got stuck trying to figure out how to pass a NodeJS string into wasm :sweat_smile:
anyone with wasm knowledge know how to go about doing that? I basically want to have main.roc
give a Str -> Str
function instead of a Str
, and then call that from JS passing a JS string converted to a wasm representation that Roc can use
Just a note, I know Luke was running into some generation issue with the LLVM backend. For List U8 -> List U8
i think it broke and generated the wrong signature.
I've had trouble making that change as well. I think it is my understanding of the pub extern fn roc__mainForHost_1_exposed_generic(_: *RocStr, _: *RocStr) void;
function which has me stumped. I keep getting stuck on a Zig unreachable
error when I try and run the wasm file that gets generated. I was hoping to copy your implementation.
I really don't know how to figure out what the function types should be for the Roc exposed stuff
I feel like I'm reaching in the dark here. I had the idea of generating the types for Rust using glue, which works well. But then I think my Zig lets me down when I try and fault find. So haven't made much progress.
Basically I keep playing around with the signature of roc__mainForHost
and I am getting different errors like,
wasm-ld: warning: function signature mismatch: roc__mainForHost_1_exposed_generic
>>> defined as (i32, i64, i32) -> void in lto.tmp
>>> defined as (i32) -> void in ../roc-serverless/examples/../platform/zig-cache/o/60f380eba017104c3db40f606e31875f/roc_app145t9T.o
I am reasonably sure it should be something like pub extern fn roc__mainForHost_1_exposed_generic(_: *RocStr, _: RocStr) void;
; but I am not sure.
well one hurdle that has to be overcome is that it relies on pub fn main()
in host.zig
and main
isn't allowed to accept arguments directly
it would need to be some other function than main
and then exported differently, but I haven't been able to figure out how to get that to work
This is what I have currently. My plan is to use STDIO, you can bind to that from JS
pub extern fn roc__mainForHost_1_exposed_generic(_: *RocStr, _: RocStr) void;
pub fn main() u8 {
// Call Roc and get the Str
var arg : RocStr = RocStr.empty();
var ret : *RocStr = &RocStr.empty();
roc__mainForHost_1_exposed_generic(ret, arg);
// Print to stdio
const stdout = std.io.getStdOut().writer();
stdout.print("Printing...", .{}) catch unreachable;
stdout.print("{s}", .{ret.asSlice()}) catch unreachable;
// value.decref();
return 0;
}
@Richard Feldman shouldn't it be similar to a browser example where it is controlled by a host.js file? Then you would setup zig the same way as that example.
I have an example here somewhere, I'll dig it out. It shows the WASM and the JS for using stdio/stdout
unfortunately that example is using main()
with no arguments - https://github.com/roc-lang/roc/blob/e520eaddccd4042432efb1cc4cf0389845efbb00/examples/platform-switching/web-assembly-platform/host.js#L48 (which compiles to _start
)
Or like this. Zig still has a main, but other functions can be called/shared with wasm module
how though? in examples like that I've done console.log
on wasm.instance.exports
and all it has is _start
including if I define another pub
function just like main
(except not named main
)
This announcing-wasi-on-workers is the example I was thinking of, unfortunately it uses a library from cloudfare. I thought it was vanilla JS.
A good mental model here is that the wasm program is a dynamic library, not an executable. You don't want a main
because the main
is in the browser or node. That runs the event loop which enters JS, which calls Wasm. So Wasm is not really an entry point.
So if you have a zig host then export some functions from it
Then when you instantiate your Wasm module pass it an object of the functions it needs to "link" to
And the exported functions will appear in exports
We have a hello world example and a virtual dom example, they both do this. Did you try copying those?
pub
is not enough, you need to export it. That's the same technique we use for our built-ins. Those are also built into a library for external foreign code to use. This is no different.
It's confusing because Zig automatically exports a function called main but no other names
For npm you should look at browser examples only, and ignore all WASI examples. WASI does the opposite of what you want, as it usually focuses on executables. But for node or browser you never want that. There is only a way for Wasm to be called from JS. There's no way for it to be main
.
OK I have some time today and tomorrow. Richard and Luke, if you like, I can spend an hour or so with each of you separately on Zoom and try to unblock you. DM me to arrange. It would be good to spread familiarity of this stuff around a bit more! If we succeed then we can post back here to let people what worked.
The chats with Luke and Richard both concluded that we have some bugs in our build system for Wasm targets.
I came across one place in particular where we are assigning a variable preprocessed_host_path
to host.zig
But that can't be right because "preprocessed" implies "compiled" but this is a source code file. We are then passing it to wasm-ld
which doesn't make sense.
I think we need to spend some time debugging that build process and create a test for it that runs in CI to keep it working, such as the Node.js integration example.
This may be correct, but only because our llvm wasm build currently is heavily tied to zig. It actually tosses out the preprocessed host and use host.zig
. The final command it runs is a command to compile host.zig
and roc_app.bc
into a wasm file. I think it was a hack to make the build process simple and get it to work for llvm wasm.
That all said, no matter what, that needs to change in the long term to separate llvm wasm from zig.
In other words, currently, llvm wasm throws away all prepocessing and instead lets zig completely control the build.
gotcha - what would it take to change that?
I don't actually know. I just happened to learn about the weird dependencies when working on my recent wasm related PR.
As far as I could tell, host.zig is passed _directly_ as an argument to wasm-ld. But wasm-ld is not a Zig compiler.
oh, rereading your comment, maybe I was getting fooled by a variable name
either way we need to change this, if only to refactor the existing behaviour to make it clear what's happening!
Heya, sorry to necro this topic, but most of the context for this conversation is here. I am very interested in platform building and specifically standalone wasm platforms for roc.
I got my first taste trying to use the nightly package to compile some of the wasm examples. I hit a few issues, dug in, and got my first PR in fixing the issues with the nightly release. I tried to get a feel for where things were though the process of opening that PR, reading the two linkers, and reading through some of the conversations here.
It appears that there is still some work to be done in the surgical linker to support my goals. I browsed through the elf linker to get an idea what the work would be and this is obviously a much larger task than the one I took on previously. Before I begin I just had a couple questions.
preprocess-host
step building a platform will still depend on some final target roc app?object
crate. That library does not support wasm. It also does not have any sort of public trait to implement its API for a wasm target we define. From the elf code it looks like we predominantly rely on the .sections()
and .relocations()
iterators / items. Would you want to just duck-type their api? Implement our own trait wrapping object
's functionality, then implement that for wasm? Try to push such an implementation back upstream before we take this on?Sorry for the barrage of questions. I hoped I could give the best idea of what I want to implement by packaging some of the broad directional strokes alongside some of the implementation details. Thanks for taking the time to read this far!
re toe stepping, definitely not.
I think there is a misconception here. The surgical linker is only for native targets.
So it is is not needed for any wasm work
We have a separate custom wasm linker that @Brian Carroll wrote and it should be mostly good to just use. Otherwise, can always fallback to the legacy wasm linker
That said, I know some of our wasm building and linking was tied into zig for simplicity. Depending on your platform goals, that may need to be unwound.
If anything, I assume this work would be built on top of the additive linker (assuming it is missing the features needed)....
Actually I think I already know what is missing. If I understand correctly, the additive linker only works with the wasm dev backend and not the wasm llvm backend. Something probably needs to be done for the llvm backend and wasm backend to both link in the same way.
Totally not stepping on toes. Any assistance would be most appreciated. I'll try and explaing my understanding of the current situation. I'm not an expert on these things, and have mostly just stumbled into these things while trying to make a change. Sorry for the long post.
I've been trying to work towards removing the platform rebuilding from the cli so its not a dependency from the compiler on the platform hosts. This will remove complexity from the compiler and make it easier to maintain and to support more host languages, by shifting the responsibility for (pre)building binaries to the platform hosts, who can use thier own tooling etc. Roc is then only responsible for linking the host and the app together.
It turns out, what I thought would be a relatively minor change is a rather large yak that requires shaving. There seems to be a lot of different things that are interrelated and need to happen first.
In short the current status is that I am stuck on https://github.com/roc-lang/roc/pull/6808, specifically I don't have a great setup on linux so it's been slow to fix this, and I've gotten distracted by other things I can make progress on. This change fixes the preprocess host subcommand, which means we can make prebuilt binaries for the platform hosts that are suitable for the surgical linker. That will enable us to land this basic-webserver and this basic-cli PRs which refactor the host out into crates and add a build script. Without the pre-process host subcommand working, we can't build the prebuilt binaries to support the surgical linker.
For WASM specifically, I have been stuck on at least one issue. I started the roc-platform-template-wasi platform to try and find the correct way to build WASM platforms. The current roc cli rebuilds a WASM host using some workarounds by compiling to llvm bytcode and then using zig to build the final .wasm
file. I think this is a limitation in zig 0.11.0
that is resolved in zig 0.12.0
and later versions. If we upgrade our zig version (which looks doable now then I think we can use zig to link two .wasm
files just using zig build-exe
or zig build-lib
. I discovered this while working on upgrading the glue for zig platforms. So ideally, we can have the host (pre)built into a .wasm
file and then all the roc cli needs to do is link that with the app .wasm
file to make the final binary. We may be able to use the WASM linker that we already have for the dev backend, I haven't looked into that yet. I haven't been able to build and link everything correctly manually yet.
After unlocking the preprocess-host subcommand, and updating the platforms to support surgical linking, we can then return to the cli and switch off platform rebuilding. But doing this breaks all of the cli platform tests in the compiler as they now need a second step to build the platform. All of those changes are mostly done and ready to go in #6696. We also want to move most of the examples from roc-lang/roc/examples
and into the examples repository at roc-lang/examples
, and the cli test that are there into roc-lang/roc/crates/cli/tests/...
. We have cli tests for CI to ensure we don't break basic-cli etc, but these examples aren't the best experience for newcomers wanting to checkout roc, that is why we have the examples repository.
Ok, yes I think there is some misconception on the surgical linker then. I tried to build the wasm platform in rust instead of zig. I was looking to build wasm plugins for a host (not roc host) rust program. To do that it was helpful to share data structures and logic between the host program and the roc platform code. So I tried to do a "standalone" wasm build, adapting the nodejs interop example to the instructions found in the go platform tutorial. I got all the way to step 4 with roc preprocess-host main.roc --target wasm32
and it failed with thread 'main' panicked at crates/linker/src/lib.rs:335:14: not yet implemented: surgical linker does not support target Wasm32
. At that point I dove into the surgical linker since that was where that error came from.
Is this the "seperate custom linker" you were referring to?
https://github.com/roc-lang/roc/blame/f8c6786502bc253ab202a55e2bccdcc693e549c8/crates/compiler/build/src/link.rs#L1196
Something probably needs to be done for the llvm backend and wasm backend to both link in the same way.
There's a common misunderstanding to watch out for. "The additive linker" is not really thing. It's not a separate entity from the wasm dev back end. Rather, that backend is designed to not really need any linking.
The backend has some internal state where it keeps track of the "instructions generated so far". To initialise that state, I "cheat" by loading it up with the instructions from the host instead. So the backend "thinks" it generated the host. It then "appends" the instructions for the Roc app to the instructions for the host.
There is not really any linking because you never have two separate things to link.
When Brendan mentioned "seperate custom linker" this is what he was referring to.
Here is the type signature of build_app_module
.
pub fn build_app_module<'a, 'r>(
env: &'r Env<'a>,
layout_interner: &'r mut STLayoutInterner<'a>,
interns: &'r mut Interns,
host_module: WasmModule<'a>,
procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>,
) -> (WasmModule<'a>, BitVec<usize>, u32)
You can see one of the arguments is host_module: WasmModule<'a>
and the return value is another, larger WasmModule<'a>
.
So it takes the host and adds the app to it.
Now I oversimplified a little when I said there is "no" linking to do. There is no linking needed for app-to-host calls. But we do need to do some linker type work for host-to-app calls. In other words, the mainForHost
call.
So there is some linking functionality there
And if someone wanted to make a separate linker for Wasm then there is plenty of relevant code there you could start from. In particular the wasm_module
crate. It knows how to parse a binary file into that WasmModule
data structure. We have Rust code for parsing and traversing the linking data in a Wasm file.
Thanks for the pointer @Brian Carroll ! I am going to have to take a hot second to digest what you wrote here. I have not been through the gen_wasm
compiler module yet.
OK cool. I'll throw some more links at you for the bits that do linking stuff.
link_host_to_app_calls
Nearly all the linking related stuff is in wasm_module rather than gen_wasm. So if someone wanted to build a separate surgical linker to use with the LLVM backend, they would import stuff from wasm_module and not gen_wasm.
Might be worth looking at the README files as well.
And the reason wasm_module
is not invoked from the linker today is it does not have produce the appropriate llvm metadata, so cannot be optimized?
The surgical linker is all about getting LLVM to optimize the final output?
No that's not right.
Optimisation happens before linking.
Also link.rs is not a linker
Sorry, just to level set. I am an overly ambitious app developer (with a background mostly in web). Not a compiler dev.
cool :thumbs_up:
most of the compiler devs here were overly ambitious app devs not that long ago. 2 years or so in my case.
Sorry, the short replies there were just cos someone was interrupting me here in the real world!
No worries! I am greatful for any feedback at all.
So the link.rs
file is part of a kind of ad-hoc build system. It makes shell commands to invoke linkers with various command line options.
Ok, so lets take a step back. Under the build instructions here I was confused about the "why" behind some of these things. My naive assumptions -> the things confusing me are:
roc gen-stub-lib
and roc build --lib main.roc --output platform/libapp.so
exist..rh
and .rm
files, but those require calling roc preprocess-host main.roc
on an app
module (which in turn references the platform you are trying to build those files for :dizzy: )
- The platform is entirely independent of the app using it. -> This makes it confusing why
roc gen-stub-lib
androc build --lib main.roc --output platform/libapp.so
exist.
I think those two commands are creating what this diagram calls the "app library" and "platform library".
- it is possible for the roc compiler to consume an independently built platform -> Prebuilt platforms require
.rh
and.rm
files, but those require callingroc preprocess-host main.roc
on anapp
module (which in turn references the platform you are trying to build those files for :dizzy: )
I think what's happening here is that you are building the platform with a dummy app that will later gets replaced with a real one. Definitely a bit roundabout, and that's because we are dealing with existing toolchains for other languages that we didn't create. They don't have command line arguments for the exact thing Roc wants to do, so we end up doing tricks like this. Most Roc app developers just download the prebuilt platform and never have to think about this weird stuff.
- host == the language a roc platform is built in, platform == the implementation of the roc platform api in a host language
platform = host + some Roc code on top to present a nice API to apps
So no the platform is not just in a host language, it is a combination of host language and Roc.
- When compiling an object is built for the roc app then "linked" via either the surgical or legacy linkers to the prebuilt platform
Yep
- I am getting most of these steps from here then trying to apply them to the couple wasm examples in the roc repo.
OK
The platform is entirely independent of the app using it. -> This makes it confusing why roc gen-stub-lib and roc build --lib main.roc --output platform/libapp.so exist.
We don't need gen-stub-lib anymore you can use roc build --lib main.roc --output platform/libapp.so
instead, the plan is to remove it. See #6696. There is currently a bug in pre-process host that overwrites the dylib, and we want to change that API.
I put together some slides to try and give an overview of platform development, you can find the thread https://roc.zulipchat.com/#narrow/stream/303057-gatherings/topic/Roc.20Online.20Meetup.20May.202024/near/440575947
WASM is a little special in some ways though.
That https://www.roc-lang.org/examples/GoPlatform/README.html example only shows how to build for surgical linking, which is not applicable for WASM -- and coupled closely with the current cli which we plan to change.
So it is not going to be a helpful guide for what you are trying to do
I would recommend you look at another platform like https://github.com/lukewilliamboswell/roc-platform-template-zig or https://github.com/lukewilliamboswell/roc-platform-template-wasi or https://github.com/lukewilliamboswell/roc-platform-template-go
Thank you both. Your comments have already been very helpful, and I still have a lot more reading to do! I get that it is still early in the project, and there are some hacks in place to use existing toolchains, but I think the work you all are doing here is really stellar. I would love to continue contributing. I will hapily take guidance If you have a good next issue for me to chew on given what we talked about today while I digest what you both have shared with me.
Just saw you moved messages @Luke Boswell
In short the current status is that I am stuck on https://github.com/roc-lang/roc/pull/6808, specifically I don't have a great setup on linux so it's been slow to fix this, and I've gotten distracted by other things I can make progress on.
My linux environment is all setup and ready to go, so this could be a good fit working towards enabling more independent platforms.
Sounds great. I've been using that PR for refactor host in basic-cli to test it. They're kind of paired, so when the PRs are ready we can do a new release of both and not block anyone. If you can look into that I would appreciate it, let me know if you have any questions. @Brendan Hansknecht has provided a lot of guidance with that API change too. @Anton has also done some investigation, but I haven't looked into that yet.
Last updated: Jul 05 2025 at 12:14 UTC