Emit @main for wasm32-none-wasm?

I am using the "Embedded" experimental feature and compiling for wasm and macOS at the same time, however swiftc is not using @main when building for wasm. I can't define my own entry function with cdecl because main is actually defined in a protocol extension, which is used to hide platform specific code.
Is there a way to tell swiftc to still emit the usual entry point for wasm-ld to find?

I am currently using this make command:

wasm:
	@swiftc -target wasm32-none-wasm -enable-experimental-feature Embedded -wmo -Xcc -fdeclspec -parse-as-library -Osize $(SOURCES) -c -o build/test-wasm.o
	@wasm-ld build/test-wasm.o -o web/src/test.wasm

Am I using the wrong flags? Swift can emit @main in embedded mode for macOS perfectly fine so it seems specific to not having an os

Can this be made to work with some flag now? My code on other platforms still works by implementing main on the protocol and I have to work around it by hardcoding the main type name.
This prevents me from using modules as the implementation code needs to know the @main type. It's also very confusing for cross compilation.

Is there a workaround for this?


Maybe I should give up and use macros here. Is there a guide/example to using macros with Embedded Swift?

1 Like

I'm not really familiar with the wasm specifics here... Would you mind explaining what is the issue or show an example setup that does not work?

1 Like
import Core

@main
struct MyGame: Game {
    func update(input: borrowing Input) {}
    func frame(input: borrowing Input, target: inout some MutableDrawable) {}
}

I have a protocol that implements main for various platforms like WebAssembly/WebGL, Cocoa/Metal or SDL2. On wasm @main doesn't do anything, it doesn't expose the entry point in any way, it doesn't even seem to emit any code.

What I wanted to do was to get @main to expose the entry point on a wasm target, though I'm not sure how async or throws would work here. Macros are more flexible for this use case and let me avoid existential types, but I couldn't get them to work with wasm before.

I was finally able to do this with a macro that replaces @main so I think this isn't an issue anymore

Outside of WASI there's no such thing as "an entry point" in Wasm. Embedded Wasm modules are practically always "libraries" and it's up to the host environment to call whatever function they want to kickstart a workload. Even WASI doesn't have "executable" or "library" concepts, but makes a distinction with more abstract "commands" and "reactors".

Because of that, it's unclear what effect @main would have in embedded Wasm. Most probably it would be a no-op.

I know, but I could just call it myself. I didn't need main to magically work, just to be exported from the module.

@Lua I'm not sure how to solve your problem with emitting main. I suppose you should just expose it and call it like you would any other library method.

However,

I am very interested in what you did to get wasm out of vanilla swiftc, especially with the Embedded feature. I've been trying for a few hours to figure out what I need to do, but I can only get it to work using swift build.

I've tried this:

@swiftc \                                                                                                 ─╯
    -target wasm32-unknown-wasi \
    -parse-as-library \
    hash.swift -o lib.wasm \
    -Xclang-linker -mexec-model=reactor \
    -Osize
error: missing external dependency '/home/alyce/.local/share/swiftly/toolchains/6.1.0/usr/lib/swift/wasi/static-executable-args.lnk'

and I tried copying what you have. The first step works, but wasm-ld ends with errors as well:

@wasm-ld build/hash.o -o web/src/hash.wasm --no-entry                                                     ─╯
wasm-ld: error: build/hash.o: undefined symbol: __stack_chk_guard
wasm-ld: error: build/hash.o: undefined symbol: posix_memalign
wasm-ld: error: build/hash.o: undefined symbol: __stack_chk_guard
wasm-ld: error: build/hash.o: undefined symbol: __stack_chk_fail

Any insight you could give we would be much appreciated. I'm just trying to implement a simple hashing algorithm in swift and the outputed wasm binary with swift build is 4.5MB after being optimized which just isn't going to work.

Hi!

I didn't push my code in a while, I'll do that soon, but it could still be useful TeamPuzel/Puzel: A collection of dependency free rendering and audio primitives for Embedded Swift. - Puzel - Gitea: Git with a cup of tea.
I use the makefile to build, I did not have time to figure out cmake for this project, so it's just badly hardcoded scripts :slight_smile:

I had to implement some C functions and I compile walloc for memory allocation, it's tiny and works.

I made a macro @Expose which handles functionality similar to what @main would do.

The binaries are indeed tiny so I recommend looking into it if that's important to you.

I can't thank you enough for sharing. I was able to copy the swiftc arguments you are using in the makefile and it when from from 4.5MB down to 2.1KB and then after removing -assert-config Debug it went down to 664bytes.

1 Like

With new Swift SDKs for Wasm there's no need anymore to maintain a set of custom options and linker invocations. Custom allocators, RNG implementation, or custom imported functions for IO aren't needed either, you get all of this working out of the box with the Embedded Swift SDK for WASI, while low level details are already handled by WASI-libc. By default the standard WASI _start global exported function is used as an entrypoint for top-level code or @main entrypoints, adjusted accordingly if you pass -mexec-model=reactor via unsafeFlags in your Package.swift.

Binary size is not significantly impacted that way, a hello world printer built with Embedded Swift SDK is still under 10 kB. I don't think there's a reason to maintain custom driver and linker invocations, especially if you're writing portable and reusable packages. Otherwise please let me know, I'd be happy to make this as smooth as possible.

Unless browsers provide the WASI libc this means you now how to send it over the network yourself, a more use-case specific setup like I decided on is still more minimal. The browser side of my code is only about 300 lines of JS and while I don't know the exact size of any WASI implementations I'm pretty sure it's impossible to make a libc even remotely that small.

It's certainly nice to have an "out of the box" solution but WASI is a different target.

If anything I'd say Swift is at fault here for how painful it is to support a zero dependency wasm setup, I work on the same game library in multiple programming languages and for my browser target the JS side is shared with the Rust version — where targeting wasm without WASI is trivial and works out of the box as well. I think Rust handles reusable no-std code perfectly fine and it's the Swift build system that's causing issues, not the target.

It's unfair to declare any use case pointless ignoring any advantages just because it doesn't magically work.

You don't need to include all of WASI-libc, compilers and linkers are smart enough to eliminate unused functions, which is what happened in that 10 kB example, which does depend on WASI-libc. As for the browser WASI implementation itself, would 3-6 kB be small enough for your use case? 300 lines of JS is unlikely to be smaller than that.

Would you mind clarifying the exact issues you're experiencing?

I'm trying to understand the advantages first. It doesn't seem to be significantly advantageous in terms of binary size, as WASI overhead is measured in single-digit kilobytes, low double digits in more complex cases. And what you get in return (allocator, RNG, I/O, clocks, timers, concurrency etc) is something you have to ship on your own anyway, which is unlikely to be any smaller. Any non-trivial application itself is usually much larger, sometimes by order of magnitude, which makes WASI overhead negligible at that point.

But let's say you were able to pinpoint a meaningful size difference for your use case and you're ready to pay for it by writing everything from scratch and discarding compatibility with other Swift packages. Are there any other advantages that I'm missing besides potential small binary size improvements?

You mentioned that it's in conflict with reusable packaging, but with Rust I found it very easy to do what I'm doing and still have access to most crates even if I don't choose to do so. If it's a problem when using Swift it must be the build/package system not the use case

You say it's discarding compatibility with other Swift packages but I think the build/package system is what's making it difficult since it's very easy with Rust's cargo.

I value the simplicity. I just wrote a little bit of code over a year ago and it's been working with no need for changes unless I add features — for the simple use case I have, which is a very small library to prototype games I can easily share with people, it's barely any code and I don't need any dependency management at all.

The interface I provide to the game being fairly high level and abstract instead of low level primitives like a libc would is intentional, it's so that I only need to provide the equivalent to those 300 lines of JS for any given platform and all games just work on it now, even if it has no libc available. I try to keep it small for that reason, not really binary size, though at the time it was since it was multiple megabytes without embedded mode.

If want it to be fairly high level and abstract in Swift, you'll need an allocator for high-level concepts like classes or Array. For Dictionary you'll need an RNG on of that. I'm assuming you're providing your own allocator and RNG, so how exactly that would keep your code small? I'm also not sure how these custom low level customisations would work on an arbitrary platform, as would have to port them to each platform yourself.

There's nothing bad in using a libc or WASI, the former gives you a stable API, the latter gives you a stable ABI (implementable for browsers in same 6-10 kB of JS that you don't have to maintain on your own). If you want your high level and abstract code to "just work", it's better to rely on standards that have been established for long time and had literally thousands of person-hours invested in them.