Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fiber switching for WebAssembly #13107

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
75c09ea
feat: Fiber switching for WebAssembly
lbguilherme Feb 22, 2023
310e67d
Merge branch 'master' into feat/wasm32-fibers
lbguilherme Feb 22, 2023
ae4746d
fix: run tests with a fresh compiler
lbguilherme Feb 22, 2023
f78eafd
feat: in release mode optimize for size, not for speed
lbguilherme Feb 23, 2023
11ba5a7
fix: typos
lbguilherme Feb 23, 2023
595e55f
chore: small refactor on wasm-opt command
lbguilherme Feb 23, 2023
cd104f3
fix: wasm32 ci
lbguilherme Feb 23, 2023
be05e33
fix: avoid stripping debug info if we are in debug mode
lbguilherme Feb 23, 2023
006c6db
fix: no need for sudo
lbguilherme Feb 23, 2023
0837426
fix ci
lbguilherme Feb 23, 2023
48af90b
fix: correctly use @debug enum
lbguilherme Feb 23, 2023
ac8f5a5
Update src/compiler/crystal/compiler.cr
lbguilherme Feb 23, 2023
f623c44
style
lbguilherme Feb 23, 2023
b7f2a96
fix: don't skip asyncify transform on imports
lbguilherme Feb 25, 2023
e72092b
fix ci making fresh compiler
lbguilherme Feb 27, 2023
559230b
Update src/fiber/context/wasm32.cr
lbguilherme Feb 27, 2023
3d0515f
ci: use a more up-to-date version of binaryen
lbguilherme Feb 27, 2023
72a7daa
Merge branch 'feat/wasm32-fibers' of github.com:lbguilherme/crystal i…
lbguilherme Feb 27, 2023
5efd984
ci: install curl
lbguilherme Feb 27, 2023
5a57893
fix: can't compress relocations while also preserving debug info
lbguilherme Feb 27, 2023
3d53b7f
fix: no need to enable all wasm features. everything we need is alrea…
lbguilherme Feb 27, 2023
9996e02
ci: build spec suite in release mode
lbguilherme Feb 27, 2023
8082fb1
fix: --all-features is in fact required because we need bulk memory o…
lbguilherme Feb 27, 2023
0e3268d
Merge branch 'master' into feat/wasm32-fibers
lbguilherme Feb 28, 2023
90ce2be
Merge remote-tracking branch 'upstream/master' into feat/wasm32-fibers
lbguilherme Mar 9, 2023
c339f44
Merge branch 'master' into feat/wasm32-fibers
beta-ziliani Mar 13, 2023
417c7b1
enable pcre2 and update wasmtime
lbguilherme Mar 13, 2023
281547a
feat: refactor Asyncify module
lbguilherme Mar 16, 2023
189466c
fix: place the main stack before global data and increase its size
lbguilherme Mar 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions .github/workflows/wasm32.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ jobs:
- name: Download Crystal source
uses: actions/checkout@v3

- name: Install wasmtime
uses: mwilliamson/setup-wasmtime-action@v2
with:
wasmtime-version: "2.0.0"
- name: Install Binaryen
run: |
apt-get update
apt-get install -y curl
curl -LO https://github.com/WebAssembly/binaryen/releases/download/version_112/binaryen-version_112-x86_64-linux.tar.gz
echo "25a94f8129a532b4d84b6fa305ae630bcbd379d56416c895b3daa48d5fb2523e binaryen-version_112-x86_64-linux.tar.gz" | sha256sum -c -
tar -xf binaryen-version_112-x86_64-linux.tar.gz
mv binaryen-version_112/bin/wasm-opt /usr/bin/

- name: Install LLVM 13
run: |
Expand All @@ -31,19 +35,27 @@ jobs:
./llvm.sh 13
ln -s $(which wasm-ld-13) /usr/bin/wasm-ld

- name: Download wasm32 libs
- name: Download wasm32-wasi libs
run: |
mkdir wasm32-wasi-libs
curl -LO https://github.com/lbguilherme/wasm-libs/releases/download/0.0.3/wasm32-wasi-libs.tar.gz
echo "cd36f319f8f9f9cd08f723d10e6ec2b92f2e44d3ce3b20344b8041386d85c261 wasm32-wasi-libs.tar.gz" | sha256sum -c -
tar -f wasm32-wasi-libs.tar.gz -C wasm32-wasi-libs -xz
rm wasm32-wasi-libs.tar.gz

- name: Build fresh compiler
run: make crystal # TODO: Remove this after next version update

- name: Build spec/wasm32_std_spec.cr
run: bin/crystal build spec/wasm32_std_spec.cr -o wasm32_std_spec.wasm --target wasm32-wasi -Duse_pcre
run: bin/crystal build spec/wasm32_std_spec.cr -o wasm32_std_spec.wasm --target wasm32-wasi --release
env:
CRYSTAL_LIBRARY_PATH: ${{ github.workspace }}/wasm32-wasi-libs

- name: Install Wasmer
uses: wasmerio/setup-wasmer@v1
with:
version: "3.1.1"

- name: Run wasm32_std_spec.wasm
run: |
wasmtime run wasm32_std_spec.wasm
wasmer run --singlepass --enable-all wasm32_std_spec.wasm
33 changes: 25 additions & 8 deletions src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ module Crystal

private def codegen(program, node : ASTNode, sources, output_filename)
llvm_modules = @progress_tracker.stage("Codegen (crystal)") do
program.codegen node, debug: debug, single_module: @single_module || @release || @cross_compile || !@emit_targets.none?
# TODO: wasm32 shouldn't require single_module. But not using it somehow messes up with imports from named modules.
program.codegen node, debug: debug, single_module: @single_module || @release || @cross_compile || !@emit_targets.none? || program.has_flag? "wasm32"
end

output_dir = CacheDir.instance.directory_for(sources)
Expand Down Expand Up @@ -315,15 +316,16 @@ module Crystal
target_machine.emit_obj_to_file llvm_mod, object_name
end

_, command, args = linker_command(program, [object_name], output_filename, nil)
print_command(command, args)
linker_commands(program, [object_name], output_filename, nil).each do |(_, command, args)|
print_command(command, args)
end
end

private def print_command(command, args)
stdout.puts command.sub(%("${@}"), args && Process.quote(args))
end

private def linker_command(program : Program, object_names, output_filename, output_dir, expand = false)
private def linker_commands(program : Program, object_names, output_filename, output_dir, expand = false)
if program.has_flag? "msvc"
lib_flags = program.lib_flags
# Execute and expand `subcommands`.
Expand Down Expand Up @@ -379,15 +381,28 @@ module Crystal
cmd = "#{cl} #{Process.quote_windows("@" + args_filename)}"
end

{cl, cmd, nil}
[{cl, cmd, nil}]
elsif program.has_flag? "wasm32"
link_flags = @link_flags || ""
{"wasm-ld", %(wasm-ld "${@}" -o #{Process.quote_posix(output_filename)} #{link_flags} -lc #{program.lib_flags}), object_names}
link_flags += " --stack-first -z stack-size=#{8 * 1024 * 1024}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should reuse Fiber::StackPool::STACK_SIZE or perhaps introduce a more generic property for this. It would also be great to allow configuration of the stack size at compile time.
Perhaps a generic CRYSTAL_STACK_SIZE environment variable could be useful for this? It would populate this value here and Fiber::StackPool::STACK_SIZE and have 8MB as default value.
Just thinking ahead, this can be implemented outside this PR. (It's probably better to extract it).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like having a CRYSTAL_STACK_SIZE environment variable! But let's do that on another PR.

link_flags += " --compress-relocations --strip-all" if @debug.none?

opt_flags = " --all-features"
opt_flags += " -g" unless @debug.none?

output = Process.quote_posix(output_filename)

[
{"wasm-ld", %(wasm-ld "${@}" -o #{output} #{link_flags} -lc #{program.lib_flags}), object_names},
{"wasm-opt", %(wasm-opt #{output} -o #{output} --asyncify #{opt_flags}), nil},
].tap do |cmds|
cmds << {"wasm-opt", %(wasm-opt #{output} -o #{output} -Oz #{opt_flags}), nil} if @release
end
else
link_flags = @link_flags || ""
link_flags += " -rdynamic"

{CC, %(#{CC} "${@}" -o #{Process.quote_posix(output_filename)} #{link_flags} #{program.lib_flags}), object_names}
[{CC, %(#{CC} "${@}" -o #{Process.quote_posix(output_filename)} #{link_flags} #{program.lib_flags}), object_names}]
end
end

Expand Down Expand Up @@ -419,7 +434,9 @@ module Crystal

@progress_tracker.stage("Codegen (linking)") do
Dir.cd(output_dir) do
run_linker *linker_command(program, object_names, output_filename, output_dir, expand: true)
linker_commands(program, object_names, output_filename, output_dir, expand: true).each do |command|
run_linker *command
end
end
end

Expand Down
106 changes: 106 additions & 0 deletions src/crystal/asyncify.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{% skip_file unless flag?(:wasm32) %}

# :nodoc:
@[Link(wasm_import_module: "asyncify")]
lib LibAsyncify
struct Data
current_location : Void*
end_location : Void*
end

fun start_unwind(data : Data*)
fun stop_unwind
fun start_rewind(data : Data*)
fun stop_rewind
end

# :nodoc:
module Crystal::Asyncify
enum State
Normal
Unwinding
Rewinding
end

@@state = State::Normal
class_getter! main_func, current_func

# Reads the stack pointer global.
def self.stack_pointer
stack_pointer = uninitialized Void*
asm("
.globaltype __stack_pointer, i32
global.get __stack_pointer
local.set $0
" : "=r"(stack_pointer))

stack_pointer
end

# Sets the stack pointer global. Use this in conjuction with unwinding the stack.
def self.stack_pointer=(stack_pointer : Void*)
asm("
.globaltype __stack_pointer, i32
local.get $0
global.set __stack_pointer
" :: "r"(stack_pointer))
end

# Wraps the entrypoint to capture and stop stack unwindings and trigger a rewind
# into the right point.
@[NoInline]
def self.wrap_main(&block)
@@main_func = block
@@current_func = block
block.call

until @@state.normal?
@@state = State::Normal
LibAsyncify.stop_unwind

if before_rewind = @@before_rewind
before_rewind.call
end

if rewind_data = @@rewind_data
@@state = State::Rewinding
LibAsyncify.start_rewind(rewind_data)
end

func = @@rewind_func.not_nil!
@@current_func = func
func.call
end
end

# Performs a stack unwind. All stack local variables will be stored in the `unwind_data` buffer.
# If a `rewind_data` buffer is provided, the stack will be rewinded into that position after unwinding.
# `rewind_func` controls the execution target to invoke after unwinding. It can be a new function, the
# main function or the currently executing function. Finally, the `before_rewind` callback can be used
# to specify some action to do after unwinding and before rewinding.
def self.unwind(
*,
unwind_data : LibAsyncify::Data*,
rewind_data : LibAsyncify::Data*?,
rewind_func : Proc(Void),
before_rewind : Proc(Void)? = nil
)
@@rewind_data = rewind_data
@@rewind_func = rewind_func
@@before_rewind = before_rewind

real_unwind(unwind_data)
end

@[NoInline]
private def self.real_unwind(unwind_data : LibAsyncify::Data*)
if @@state.rewinding?
@@state = State::Normal
LibAsyncify.stop_rewind
return
end

@@state = State::Unwinding
LibAsyncify.start_unwind(unwind_data)
end
end
7 changes: 5 additions & 2 deletions src/crystal/scheduler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,11 @@ class Crystal::Scheduler
end

protected def yield : Nil
# TODO: Fiber switching and libevent for wasm32
{% unless flag?(:wasm32) %}
{% if flag?(:wasm32) %}
# TODO: event loop for wasm32
enqueue @current
reschedule
{% else %}
sleep(0.seconds)
{% end %}
end
Expand Down
7 changes: 6 additions & 1 deletion src/crystal/system/wasi/main.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "./lib_wasi"
require "crystal/asyncify"

# This file serve as the entrypoint for WebAssembly applications compliant to the WASI spec.
# See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md.
Expand Down Expand Up @@ -29,5 +30,9 @@ end

# `__main_argc_argv` is called by wasi-libc's `__main_void` with the program arguments.
fun __main_argc_argv(argc : Int32, argv : UInt8**) : Int32
main(argc, argv)
ret = 0
Crystal::Asyncify.wrap_main do
ret = main(argc, argv)
end
ret
end
2 changes: 1 addition & 1 deletion src/fiber.cr
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class Fiber
end

# :nodoc:
def run
def run : Nil
GC.unlock_read
@proc.call
rescue ex
Expand Down
Loading