Stream: compiler development

Topic: Building and linking WASM


view this post on Zulip Luke Boswell (May 03 2024 at 05:49):

I'm trying to build a wasm host and app manually to check I really understand the process.

I've been following along closely with the implementation in crates/compiler/build/src/link.rs specifically for build_zig_host_wasm32 and link_wasm32, and experimenting with a bunch of things.

I dont have an easy way to reproduce this rn, though I guess I could make a random commit if that would help.

But basically I'm running the following commands, but having some issues. This is the furthest I've managed to get I think (it's hard to follow the errors and see if I'm progressing or regressing sometimes).

# compile host into a LLVM .bc file
zig build-obj -target wasm32-wasi-musl -lc -rdynamic -femit-llvm-ir=platform/host.bc host/main.zig

# compile app into a LLVM .ll file
roc build --no-link --emit-llvm-ir rocLovesWebAssembly.roc

# link together host and app
zig build-exe -target wasm32-wasi-musl -fPIC -fstrip -rdynamic -lc platform/host.bc rocLovesWebAssembly.ll

My understanding here is that we are successfuly building the host.zig into a LLVM bitcode .bc file (I'm still not 100% sure why we need to go that route, but I can't get a library or anything else working either).

Roc is generating a LLVM human readable bitcode .ll file.

When we link these together to build a .wasm file the implementation of symbols in compiler-rt is slightly different, and we are missing setjmp and longjmp.

I am looking for any ideas why the roc generated code might be missing these, or different to zig. I'm using zig version 0.11.0 for everything. Do we include our own version of compiler-rt? playing with zig's -fcompiler-rt doesn't seem to make any difference. Maybe we have included those in the builtins or something?

Also unsure why setjmp and longjmp are undefined. From what I can tell these should be included in musl.

error: wasm-ld:
    note: defined as (i32, i32, i32) -> i32 in /Users/luke/.cache/zig/o/e1ad72288e7681c39fb42e87c2506cfb/libcompiler_rt.a(/Users/luke/.cache/zig/o/e1ad72288e7681c39fb42e87c2506cfb/libcompiler_rt.a.o)
    note: defined as (i32, i32, i64) -> i32 in /Users/luke/.cache/zig/o/8e32b79d2f74ce8703ec76103c3fa0de/rocLovesWebAssembly.o
error: wasm-ld:
    note: defined as (i32, i32, i32) -> i32 in /Users/luke/.cache/zig/o/e1ad72288e7681c39fb42e87c2506cfb/libcompiler_rt.a(/Users/luke/.cache/zig/o/e1ad72288e7681c39fb42e87c2506cfb/libcompiler_rt.a.o)
    note: defined as (i32, i32, i64) -> i32 in /Users/luke/.cache/zig/o/8e32b79d2f74ce8703ec76103c3fa0de/rocLovesWebAssembly.o
error: wasm-ld: /Users/luke/.cache/zig/o/8e32b79d2f74ce8703ec76103c3fa0de/rocLovesWebAssembly.o: undefined symbol: setjmp
error: wasm-ld: /Users/luke/.cache/zig/o/8e32b79d2f74ce8703ec76103c3fa0de/rocLovesWebAssembly.o: undefined symbol: longjmp

view this post on Zulip Luke Boswell (May 03 2024 at 05:56):

If I update the final step to zig build-exe -target wasm32-wasi-musl -lc platform/host.bc rocLovesWebAssembly.ll, now I get;

LLD Link... warning(link): unexpected LLD stderr:
wasm-ld: warning: function signature mismatch: memcpy
>>> defined as (i32, i32, i64) -> i32 in /Users/luke/.cache/zig/o/7f59f835ce3672f0de84e2fa02508ea0/rocLovesWebAssembly.o
>>> defined as (i32, i32, i32) -> i32 in /Users/luke/.cache/zig/o/8b8b8ee442982e6b4064dc7bd089866a/libc.a(/Users/luke/.cache/zig/o/10a7521927d8b2330ade4b85f73bcd27/memcpy.o)

wasm-ld: warning: function signature mismatch: memset
>>> defined as (i32, i32, i64) -> i32 in /Users/luke/.cache/zig/o/7f59f835ce3672f0de84e2fa02508ea0/rocLovesWebAssembly.o
>>> defined as (i32, i32, i32) -> i32 in /Users/luke/.cache/zig/o/8b8b8ee442982e6b4064dc7bd089866a/libc.a(/Users/luke/.cache/zig/o/46d827fde6ae7406d7854e8a8727cccb/memset.o)

view this post on Zulip Luke Boswell (May 03 2024 at 05:56):

I think removing -rdynamic solves the setjmp and longjmp issue

view this post on Zulip Luke Boswell (May 03 2024 at 06:07):

It all seems far more complicated than I had expected. I thought we would compile the host to a .wasm file and then the app too, and link them together.

view this post on Zulip Luke Boswell (May 03 2024 at 06:17):

If I use zig build-exe -target wasm32-wasi  host/main.zig I get a nice wasm file, that I can inspect with wasmer inspect.

But roc build --target=wasm32 --no-link rocLovesWebAssembly.roc gives me a file that seems broken.

$ wasmer inspect rocLovesWebAssembly.wasm
error: failed to inspect `rocLovesWebAssembly.wasm`
╰─▶ 1: WebAssembly translation error: Error when converting wat: input bytes aren't valid utf-8

Sharing all this here... because I don't really know which way is up, and trying to make sense of things.

view this post on Zulip Luke Boswell (May 03 2024 at 06:26):

Actually... digging some more, these issues may all just be from using LLVM backend. If we switch over to look at the dev backend then it looks like we are using roc_wasm_module. So maybe the question I should be asking is how to preprocess a wasm host for the surgical linker?

view this post on Zulip Folkert de Vries (May 03 2024 at 07:44):

re that wasmer thing: something seems confused between the text and binary formats there

view this post on Zulip Folkert de Vries (May 03 2024 at 07:45):

wat is the webassembly text format, that lisp-like syntax for wasm. What we produce is a binary file.

view this post on Zulip Luke Boswell (May 03 2024 at 08:35):

Yeah, seems strange.

view this post on Zulip Luke Boswell (May 03 2024 at 08:35):

Do you think the WASM examples should focus on support for legacy or surgical linker or both?

view this post on Zulip Richard Feldman (May 03 2024 at 11:01):

unfortunately we need both for now

view this post on Zulip Folkert de Vries (May 03 2024 at 11:12):

do we? I thought wasm was pretty much complete too? what breaks when we feed it a module produced by LLVM?

view this post on Zulip Brian Carroll (May 10 2024 at 19:36):

Instead of wasmer inspect, try to get your hands on wasm-objdump and wasm-validate, which are part of a project called.. binaryen I think

view this post on Zulip Brian Carroll (May 10 2024 at 20:14):

Or maybe wabt

view this post on Zulip Luke Boswell (May 10 2024 at 20:16):

Thank you.

view this post on Zulip Luke Boswell (May 10 2024 at 20:16):

Ill give that a go

view this post on Zulip Brian Carroll (May 12 2024 at 06:45):

Actually wasmer inspect works just fine for me on files that I know for sure have binary webassembly inside them.
I wonder if you somehow have a file that has the text format inside it but with a .wasm extension which is for the binary format.

view this post on Zulip Brian Carroll (May 12 2024 at 06:52):

Do we include our own version of compiler-rt?

As part of building the Roc project, crates/wasi-libc-sys/build.rs uses Zig to generate compiled libc and compiler-rt, finds their paths, and sets constants equal to those paths.
Then in the linker commands in crates/compiler/build/src/link.rs we provide wasm-ld with those paths as well as the paths to the host and the Roc app. See preprocess_host_wasm32.

view this post on Zulip Brian Carroll (May 12 2024 at 06:57):

In the early days this build process only worked when building Roc from source. But I think later we made it work with nightly builds too. Could be worth double checking that though.

view this post on Zulip Brian Carroll (May 12 2024 at 07:05):

How did setjmp end up in the roc binary? Shouldn't that be a host-side thing? I have been a bit out of touch with Roc lately, might have missed some big change.

view this post on Zulip Brian Carroll (May 12 2024 at 07:41):

I don't think it's actually possible to implement setjmp and longjmp in the Wasm instruction set. There are no arbitrary jumps, only structured control flow. So the compiler would have to generate early returns in every function all the way up the call stack. And then I'm not sure why you would even need functions with those names. So maybe one of these errors is happening because they don't exist in this target?
I just downloaded a zip of the Zig repo at tag 0.11.0 and searched for references to setjmp in wasm libc and compiler-rt files etc., and I can't find anything.

view this post on Zulip Brian Carroll (May 12 2024 at 07:50):

One of the error messages complains about a mismatch in last argument of memcpy and memset so we can look up cppreference.com to find out the actual type signatures.

void* memcpy( void *dest, const void *src, size_t count );
void *memset( void *dest, int ch, size_t count );

The last argument for each is a size_t. That's a pointer-sized integer, so 32 bits is the correct size for that in Wasm. Whichever one is generating 64 is wrong. Most likely there is somewhere we forgot about 32-bit targets and generated size_t as 64 bits. This is very common. Everyone forgets about 32 bits. It's the root cause of 99% of code gen issues with Wasm.

view this post on Zulip Richard Feldman (May 12 2024 at 10:46):

@Folkert de Vries any idea on the setjmp part of that question?

view this post on Zulip Folkert de Vries (May 12 2024 at 10:50):

I think this message is pretty clear

// Utils continued - SJLJ
// For tests (in particular test_gen), roc_panic is implemented in terms of
// setjmp/longjmp. LLVM is unable to generate code for longjmp on AArch64 (https://github.com/roc-lang/roc/issues/2965),
// so instead we ask Zig to please provide implementations for us, which is does
// (seemingly via musl).
pub extern fn setjmp([*c]c_int) c_int;
pub extern fn longjmp([*c]c_int, c_int) noreturn;
pub extern fn _setjmp([*c]c_int) c_int;
pub extern fn _longjmp([*c]c_int, c_int) noreturn;
pub extern fn sigsetjmp([*c]c_int, c_int) c_int;
pub extern fn siglongjmp([*c]c_int, c_int) noreturn;
pub extern fn longjmperror() void;

view this post on Zulip Folkert de Vries (May 12 2024 at 10:51):

so: tests need an implementation of roc_panic!. There is not much we can do there beside a longjmp really

view this post on Zulip Luke Boswell (May 12 2024 at 18:05):

@Brian Carroll what are your thoughts on the best way to pre-build a WASM platform? I'm updating roc so that the platform rebuilding is the sole responsibility of the platform, which provides the pre-built binaries. What should this look like for WASM though?

view this post on Zulip Brian Carroll (May 12 2024 at 18:14):

What are you doing for the others and how is Wasm different?

view this post on Zulip Brian Carroll (May 12 2024 at 18:19):

Actually let me rephrase that as a statement rather than a question! I don't think there's any conceptual difference with Wasm, it just uses a different linker. And you can't rely on the OS to provide libc for you.

view this post on Zulip Brian Carroll (May 12 2024 at 18:21):

So I assume that what you want to produce for all targets, including Wasm, is a library that contains:

view this post on Zulip Brian Carroll (May 12 2024 at 18:22):

Although for most targets you can make the library depend on the host machine's libc. For Wasm you have to link that in.

view this post on Zulip Luke Boswell (May 12 2024 at 18:22):

I've converted all the others in https://github.com/roc-lang/roc/pull/6696

view this post on Zulip Luke Boswell (May 12 2024 at 18:22):

I've written a build script for each of the different platforms.

view this post on Zulip Brian Carroll (May 12 2024 at 18:23):

Can you outline it for me so I don't have to go through the whole PR?

view this post on Zulip Luke Boswell (May 12 2024 at 18:23):

Most of these only support the legacy linker though... so I'm generating a macos-arm64.a file for the platform prebuilt-binary.

view this post on Zulip Luke Boswell (May 12 2024 at 18:24):

That c-archive sits next to the platform/main.roc file and then when we roc run app.roc the roc cli will build the app, link with the platform to produce the final application binary

view this post on Zulip Luke Boswell (May 12 2024 at 18:26):

I'm just not sure about WASM, in that the same (or similar) approach seems to require jumping through a lot of hoops. If it should be the same, and we just have a bug somewhere to track down then that is very helpful to know.

view this post on Zulip Brian Carroll (May 12 2024 at 18:26):

What hoops are those?

view this post on Zulip Luke Boswell (May 12 2024 at 18:27):

Well to support the legacy linker at least. I suspect the surgical linker would be much easier

view this post on Zulip Luke Boswell (May 12 2024 at 18:29):

For example, we need to compile the host into a LLVM .bc file, instead of just a c-archive

view this post on Zulip Brian Carroll (May 12 2024 at 18:30):

OK why is that?
I was going to say I haven't seen .a used very much with wasm. But not sure what constraint actually forces this because it's just a concatenation of object files.

view this post on Zulip Brian Carroll (May 12 2024 at 18:38):

I would have guessed you could create a .wasm file, or a .a file, containing everything you need.
I don't see why .bc is necessary unless LLVM is for some reason forcing that.
But I am not super familiar with trying to get LLVM to produce Wasm in various ways. Most of my experience with this stuff is from working on the Wasm dev back end with no LLVM in the path.

view this post on Zulip Brian Carroll (May 12 2024 at 18:39):

The Wasm dev backend has no understanding whatsoever of .bc or .ll so that would be incompatible with this setup.

view this post on Zulip Brian Carroll (May 12 2024 at 18:40):

Essentially nobody would ever be able to use the Wasm dev backend because the ecosystem would be incompatible with it.

view this post on Zulip Brian Carroll (May 12 2024 at 18:41):

So I think it would be worth trying to find a way to use .wasm as that's the only thing the dev backend can work with

view this post on Zulip Brian Carroll (May 12 2024 at 18:43):

You can look at what I've done for the wasm unit tests or the wasm REPL. They both have "test hosts" that I build into .wasm files.

view this post on Zulip Brian Carroll (May 12 2024 at 18:45):

crates/repl_wasm/build.rs build_wasm_platform
crates/compiler/test_gen/build.rs build_wasm_test_host

view this post on Zulip Brendan Hansknecht (May 13 2024 at 06:26):

I think currently we generated a .bc and then let llvm directly consume the .bc file cause it was easy and it worked around some early issue when setting up wasm linking with the llvm backend (@Folkert de Vries may know what the issue actually was). That said, it is just a hack, our over-reliance on zig for llvm backend wasm linking is also a dependency we need to fix. Fundamentally, I think most of that is old tech debt that we really need to wrip out a clean up

view this post on Zulip Brendan Hansknecht (May 13 2024 at 06:27):

Wasm should just generate a/multiple .wasm file and let the additive linker concatenate them together.

view this post on Zulip Brendan Hansknecht (May 13 2024 at 06:28):

That said, last time I checked, the additive linker could only be used with our wasm dev backend and had some sort of issue linking wasm files generated form the llvm backend (but that might just be a setup/wiring issue and not a real bug, been a long time since I looked at any of this).

view this post on Zulip Brendan Hansknecht (May 13 2024 at 06:28):

Even if we don't change the llvm backend to use the additive linker, it should still generate a .wasm and that should be consumed by the platform or by a linker of some sort to generate the final executable. The .bc stuff all needs to die and is just a workaround.

view this post on Zulip Brian Carroll (May 13 2024 at 06:31):

Yes "additive linking" is a term I came up with for the linking that is built into the wasm dev back end. It is a feature of that back end
It is not possible to extract as a separate thing and use with LLVM.

view this post on Zulip Brendan Hansknecht (May 13 2024 at 06:32):

ah. Thought it could be generalized to anything wasm...guess not

view this post on Zulip Brian Carroll (May 13 2024 at 06:32):

It works by reading in the host file and using that to initialise the state of the code generator so that when you generate more code it is "added on top".
We do not have that kind of access to the internals of LLVM.

view this post on Zulip Brendan Hansknecht (May 13 2024 at 06:34):

I see. Yeah, I thought it came at a later stage and was consuming fully generated functions from the wasm dev backend. Thus, I assumed we could pass full wasm functions from llvm to be added on top of the wasm platform. Good to know that is not the case and it is more integrated.

view this post on Zulip Brendan Hansknecht (May 13 2024 at 06:35):

yeah, so we still need to figure out a good linking story for llvm wasm (that or leave it 100% to the end users). Today, IIRC, we compile to a .bc and then pass the .bc to zig such that it can build the host with the .bc file and generate a finalized platform.

view this post on Zulip Brendan Hansknecht (May 13 2024 at 06:35):

So we essentially use .bc in a way to avoid needing to figure out correctly linking.

view this post on Zulip Brendan Hansknecht (May 13 2024 at 06:35):

leave it all to zig

view this post on Zulip Brian Carroll (May 13 2024 at 06:35):

I believe I might have solved that problem already in the build.rs files I mentioned above. Or at least a very similar problem.

view this post on Zulip Brian Carroll (May 13 2024 at 06:39):

They produce a preprocessed host as .wasm file containing the host, the Roc builtins, and any libc stuff the host and builtins need.
So then with LLVM Wasm you would just need to do a final linking step with wasm-ld to combine the preprocessed host .wasm file with the app .wasm file.
And the Wasm dev backend can also use it by having the preprocessed host passed via CLI options. That's already implemented.

view this post on Zulip Brian Carroll (May 13 2024 at 06:49):

By the way, a .wasm file is most comparable to .so on Linux or .dylib on Mac. By definition of how Wasm works, it always needs to receive some callback functions from the Wasm interpreter at runtime. And that is a dynamic linking step.

view this post on Zulip Brian Carroll (May 13 2024 at 06:51):

So they are always dynamic libraries, and they can be linked together. The "imports" and "exports" get merged, and sometimes one export satisfies another import and it gets removed from the final file. In the end you are left with just the ones that actually need to be exposed at runtime.

view this post on Zulip Richard Feldman (May 14 2024 at 08:36):

is it possible to do stack unwinding in wasm?

view this post on Zulip Brian Carroll (May 14 2024 at 08:36):

No

view this post on Zulip Brian Carroll (May 14 2024 at 08:38):

If you had enough power to do that, it would break the security model.

view this post on Zulip Richard Feldman (May 14 2024 at 08:38):

hm, I see

view this post on Zulip Richard Feldman (May 14 2024 at 08:39):

I was trying to think of other ways we could potentially make roc_panic work in wasm

view this post on Zulip Richard Feldman (May 14 2024 at 08:39):

since setjmp/longjmp don't work

view this post on Zulip Brian Carroll (May 14 2024 at 08:39):

You can "trap" and catch outside and call again

view this post on Zulip Richard Feldman (May 14 2024 at 08:40):

ah gotcha

view this post on Zulip Richard Feldman (May 14 2024 at 08:41):

so that works as long as you don't need to recover from the roc_panic from within wasm code

view this post on Zulip Richard Feldman (May 14 2024 at 08:42):

I guess the exception handling proposal could address that in the future

view this post on Zulip Folkert de Vries (May 14 2024 at 09:59):

for tests we don't need to recover from roc_panic, so if setjmp/longjmp in the compiler is a problem we can explore that?

view this post on Zulip Luke Boswell (May 20 2024 at 04:46):

I need to write up a bit of a summary -- but this is where I got to on the plane. https://github.com/lukewilliamboswell/roc-platform-template-wasi

view this post on Zulip Luke Boswell (May 20 2024 at 04:51):

Basically, I think the way we do linking from roc cli makes this a bit interesting and unique. Most tooling i.e. zig is geared towards making a standalone .wasm library. So a bit of a challenge if we want roc to drive the build/linking. If we build the app separately using roc build --target=wasm32 --no-link app.roc and then use zig or something else to drive the build I think things get much simpler (but not the end user experience consistent with the other platforms).

I think the next step is to figure out how to manually create the _start symbol, so when we link using e.g. something like /opt/homebrew/opt/llvm@16/bin/wasm-ld app.wasm platform/host.wasm -o out.wasm it has everything required. Once we can build the pre-built binary and link manually, we will need to revisit the linking in roc cli if we want to support this workflow.

Mostly leaving this comment to remind my future self.

view this post on Zulip Brian Carroll (May 20 2024 at 06:24):

That all sounds good
I don't think you should have to manually create _start. The toolchain should be doing that for you if configured to generate a WASI executable. But this is the sort of thing that can be tricky to set up.

view this post on Zulip Luke Boswell (May 20 2024 at 07:15):

Thank you. I think I need to investigate it further. From what I can tell so far it's not generated by zig with b.addSharedLibrary but it is with b.addExecutable. So that's why my current hypothesis is we may need to generate that manually.

view this post on Zulip Luke Boswell (May 20 2024 at 07:19):

As an aside to resolve the function signature mismatch: memset issue, I changed the void return in our zig extern from extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; to extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) i32;.

It works now and is happy.

But, I'm just wondering if that is something we should be changing in the other zig platforms too, or maybe it is actually a symptom of another bug elsewhere.

view this post on Zulip Luke Boswell (Jun 29 2024 at 03:28):

I'm having trouble working with WASM things. I think our build pipeline is a bit broken at the moment.

If I have two files, platform/main.roc and main.roc and I build this using roc build --target=wasm32 --no-link main.roc on current main I should get valid WASM right?

platform "wasm"
    requires {} { main : Str }
    exposes []
    packages {}
    imports []
    provides [mainForHost]

mainForHost : Str
mainForHost = main
app [main] {
        pf: platform "platform/main.roc",
    }

main = "hi"
$ roc build --target=wasm32 --no-link main.roc
0 errors and 0 warnings found in 53 ms
 while successfully building:

    main.wasm
$ wasmer inspect main.wasm
error: failed to inspect `main.wasm`
╰─▶ 1: WebAssembly translation error: Error when converting wat: input bytes aren't valid utf-8
$ wasmer validate main.wasm
error: failed to validate `main.wasm`
╰─▶ 1: `wasmer validate` only validates WebAssembly files

Also, it doesn't seem to like the dev backend.

$ roc build --target=wasm32 --no-link --dev main.roc
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 'main' panicked at crates/compiler/build/src/program.rs:499:9:
Failed to read host object file platform/wasm32.rh! Try omitting --prebuilt-platform
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

view this post on Zulip Luke Boswell (Jun 29 2024 at 04:37):

Ok, update on progress with https://github.com/lukewilliamboswell/roc-platform-template-wasi :smiley:

I think I am now really close to having a working WASM platform -- specifically, a WASI platform using a Task based API. :fingers_crossed:

I updated the repository to use shell.nix so it provides zig 13. (I spent ages trying to get a flake working, but alas no joy... :sad: )

Updated the glue script to use the latest release of roc-glue-code-gen.

Fixed up build.zig and main.zig with some minor changes for zig 13.

Now we have it building the host and it looks good to me.

$ roc build.roc
🎉 Generated type declarations in:

    host/

$ wasmer inspect platform/host.wasm
Type: wasm
Size: 13.3 KB
Imports:
  Functions:
    "wasi_snapshot_preview1"."args_get": [I32, I32] -> [I32]
    "wasi_snapshot_preview1"."args_sizes_get": [I32, I32] -> [I32]
    "wasi_snapshot_preview1"."fd_write": [I32, I32, I32, I32] -> [I32]
    "wasi_snapshot_preview1"."proc_exit": [I32] -> []
  Memories:
    "env"."memory": not shared (1 pages..)
  Tables:
    "env"."__indirect_function_table": FuncRef (1..)
  Globals:
    "env"."__stack_pointer": I32 (mutable)
    "env"."__memory_base": I32 (constant)
    "env"."__table_base": I32 (constant)
    "GOT.mem"."__heap_end": I32 (mutable)
    "GOT.mem"."__heap_base": I32 (mutable)
Exports:
  Functions:
    "__wasm_apply_data_relocs": [] -> []
    "_start": [] -> []
    "main": [I32, I32] -> [I32]
    "roc_alloc": [I32, I32] -> [I32]
    "roc_realloc": [I32, I32, I32, I32] -> [I32]
    "roc_dealloc": [I32, I32] -> []
    "roc_panic": [I32, I32] -> []
    "roc_dbg": [I32, I32, I32] -> []
    "roc_memset": [I32, I32, I32] -> [I32]
    "roc_fx_stdoutLine": [I32, I32] -> []
  Memories:
  Tables:
  Globals:

The problem is I can't get roc to produce valid WASM for the app using --no-link to then link and verify things manually. I would like to be able to do things manually, before hacking away at the cli build pipeline.

The cli is definitely not working yet... it tries to rebuild the host using host.zig, or tries to use the surgical linker, or some other combination of things that all fail. Some examples below.

$ roc build --target=wasm32 app.roc
🔨 Rebuilding platform...
error: unrecognized file extension of parameter 'glue'
thread 'main' panicked at crates/compiler/build/src/program.rs:1048:17:
not yet implemented: gracefully handle `ld` (or `zig` in the case of wasm with --optimize) returning exit code Some(1)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

$ roc build --target=wasm32 --dev app.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 crates/compiler/build/src/link.rs:1410:21:
Error:
    Failed to rebuild platform/wasm32.rh:
        The executed command was:
            zig wasm-ld /tmp/host_bitcodecUbER9HV.wasm platform/main.o /Users/luke/Documents/GitHub/roc/target/release/lib/wasi-libc.a /Users/luke/Documents/GitHub/roc/target/release/build/wasi_libc_sys-0c00ed7e69066201/out/zig-cache/o/fbe76c5baee393dea3c7d358da7d00e5/libcompiler_rt.a -o platform/wasm32.rh --export-all --no-entry --import-undefined --relocatable
        stderr of that command:
            wasm-ld: error: cannot open platform/main.o: No such file or directory

note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' panicked at crates/compiler/build/src/program.rs:927:18:
Failed to (re)build platform.: Any { .. }

$ roc build --target=wasm32 --prebuilt-platform app.roc
Because I was run with --prebuilt-platform, I was expecting this file to exist:

    platform/host.zig

However, it was not there!

If you have the platform's source code locally, you may be able to generate it by re-running this command omitting --prebuilt-platform

view this post on Zulip Luke Boswell (Jun 29 2024 at 04:45):

I spent ages trying to get a flake working, but alas no joy...

I really wanted something that would give me a shell with the correct versions of roc and zig. It feels like something nix can do, based on the flakes we have in basic-cli and basic-webserver

view this post on Zulip Luke Boswell (Jun 29 2024 at 05:12):

So I think the next steps with this are to;

  1. figure out how to get roc to build an app with --no-link and produce valid WASM
  2. use that to manually link with the host and confirm it produces a valid WASI executable
  3. modify the roc cli to use either the dev backend parts, or wasm-ld, so that it can link a prebuilt .wasm host instead of building that from source. Maybe it defaults to using the prebuilt platform and only tries to rebuild the host if it cant find a platform/host.wasm or something

view this post on Zulip Brian Carroll (Jun 29 2024 at 06:17):

Great progress Luke! :smiley:

view this post on Zulip Brian Carroll (Jun 29 2024 at 06:53):

Those next steps make sense.

At step 2 I'd be curious to know how long wasm-ld takes to link the app and host. If it turns out to be really fast maybe there'll be no need to make our own surgical linker for Wasm (from pieces of the dev backend). The decision might be different from native code.

Maybe we can also run the same app through the dev backend and dump some timing info.

view this post on Zulip Brian Carroll (Jun 29 2024 at 06:58):

It looks like wasmer validate is not useful for figuring out _why_ a file is invalid, and debugging it. I always used wasm-objdump for that.

view this post on Zulip Brian Carroll (Jun 29 2024 at 07:10):

It's part of the WebAssembly Binary Toolkit, wabt

view this post on Zulip Ryan Barth (Jun 29 2024 at 07:18):

I was working a little with Luke here. Here is the relevant excerpt from our DMs
image.png

view this post on Zulip Ryan Barth (Jun 29 2024 at 07:19):

When the llvm compiles wasm currently it is emitting the LLVM bytecode directly to the wasm file because of that 2nd branch

view this post on Zulip Ryan Barth (Jun 29 2024 at 07:20):

allowing wasm to follow the same branch as native code (plus a little bit of feature flag cleanup) results in this
https://github.com/roc-lang/roc/pull/6852

view this post on Zulip Ryan Barth (Jun 29 2024 at 07:20):

I am able to compile the nodejs interop example without linking

view this post on Zulip Ryan Barth (Jun 29 2024 at 07:21):

obviously there is some stuff missing from that output (but at least it builds in <10ms even with a debug build of the compiler :laughing: )

view this post on Zulip Ryan Barth (Jun 29 2024 at 07:23):

Most notably it is missing the bump allocator that gets inlined to the wasm output of the gen_wasm module

view this post on Zulip Brian Carroll (Jun 29 2024 at 09:02):

Good job debugging that :+1:
Makes sense now why it wasn't valid Wasm if it was actually LLVM!

view this post on Zulip Brian Carroll (Jun 29 2024 at 09:02):

I didn't follow what you meant in the last part about the bump allocator. Do you mean that the output file has no roc_alloc? Does the nodejs host use a bump allocator?

view this post on Zulip Anton (Jun 29 2024 at 09:52):

I really wanted something that would give me a shell with the correct versions of roc and zig. It feels like something nix can do, based on the flakes we have in basic-cli and basic-webserver

In the inputs section you'll need to change the line nixpkgs.follows = "roc/nixpkgs"; to nixpkgs.url = "github:nixos/nixpkgs?rev=ab7cf5d23c90ee5b83444e0a80a606688d278ecd"; if you want to be able to get zig 13. The package is just named zig in that case, not zig_0_13.

view this post on Zulip Ryan Barth (Jun 30 2024 at 18:47):

Ok, I am getting very close on this PR. At this point I think I am running into artificial limitations / panics in the compiler. I can hand generate the unlinked app, host, wasi-libc and link them. Which basically leaves the platform. I cannot just roc build platform/main.roc --target=wasm32 --no-link --optimize. Is there a reason for or do we want to keep this limitation?

view this post on Zulip Ryan Barth (Jun 30 2024 at 18:53):

I also have questions about how much of the wasi platform we include in the compiler. It seems like the entire wasm_interp/src/wasi.rs should be pulled out into a seperate wasi platform. This would simplefy a bunch. You would not have to ship a wasi-libc. You would get freestanding wasm for free since now since wasi is not baked into the compiler. There could be a yet another wasm freestanding platform.

view this post on Zulip Ryan Barth (Jun 30 2024 at 18:54):

Another platform author could provide their own wasi implementation that does not depend on libc

view this post on Zulip Brian Carroll (Jun 30 2024 at 19:06):

Wasm interp is not part of the compiler, we just use it for our tests

view this post on Zulip Ryan Barth (Jun 30 2024 at 19:28):

My mistake. I was wondering what was providing all this in the final wasm binary. It must be wasi-libc.a then?

view this post on Zulip Ryan Barth (Jun 30 2024 at 19:30):

I just started hunting for the symbols in the roc codebase and hit that module

view this post on Zulip Brian Carroll (Jun 30 2024 at 21:12):

Yeah there's a crate called something like wasi-libc-sys. It contains almost no code, just a build.rs that fools zig into giving us a path to its libc implementation.

view this post on Zulip Brian Carroll (Jun 30 2024 at 21:15):

If an app doesn't use anything from libc then the linker just won't need to use that library and that's it.

view this post on Zulip Ryan Barth (Jun 30 2024 at 22:51):

So check this out. Platformless Roc.

https://github.com/roc-lang/roc/pull/6852/files

You can check it out and run:

export PROJ=$(pwd)
cargo build
cd examples/nodejs-interop/noplatform
bash doit.sh

Running this gets you:

+ /home/ryan/src/roc-lang/roc/target/debug/roc build main.roc --target=wasm32 --optimize --no-link
0 errors and 0 warnings found in 160 ms
 while successfully building:

    main.wasm
+ wasm-ld main.wasm -o out.wasm --export-all --no-entry --export-table --import-undefined
+ node nolink.js
@om Roc! 3

Now obviously in my haste to throw this together and experiment i messed up my pointers somewhere (probably not taking alignment into account). But I see it! Roc performs the computation and spits out the result without a (compiled) platform in wasm! This also omits wasi-libc.a and the compiler_rt.a (though admittedly i do not know what this one does) from the linking process.

So basically I forked and was experimenting with the nodejs-interop/wasm example. I used --no-link and --optimize to build an unlinked main.wasm. I have to run wasm-ld to populate the global memory and stack pointer as well as publicly expose the roc functions.


Last updated: Jul 06 2025 at 12:14 UTC