Skip to content

Allow releasing GVL when calling exported Wasm functions#603

Open
omohokcoj wants to merge 2 commits into
bytecodealliance:mainfrom
omohokcoj:main
Open

Allow releasing GVL when calling exported Wasm functions#603
omohokcoj wants to merge 2 commits into
bytecodealliance:mainfrom
omohokcoj:main

Conversation

@omohokcoj
Copy link
Copy Markdown

This PR adds a gvl: boolean kwarg to Extern#to_func. With .to_func(gvl: false) the GVL is released during the Wasm call, enabling true parallelism in Ruby.

func = instance.export("heavy_func").to_func(gvl: false)
func.call(arg) # GVL released during this call

Copilot AI review requested due to automatic review settings May 30, 2026 14:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an opt-in path to release Ruby’s GVL during Wasm function calls (via to_func(gvl: false)), while ensuring Ruby host callbacks still execute with the GVL held.

Changes:

  • Extend Extern#to_func to accept gvl: and return a Func configured to release the GVL during call.
  • Add nogvl/with_gvl helpers and wire them into Wasm calls and host-callback closures.
  • Add unit specs covering correctness, threading behavior, host callbacks, and exception propagation under GC stress.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
spec/unit/func_spec.rb Adds specs validating GVL release behavior and callback/exception correctness under concurrency/GC stress.
ext/src/ruby_api/instance.rs Updates Func::invoke callsite to pass the new GVL flag parameter.
ext/src/ruby_api/func.rs Adds per-Func GVL configuration, releases GVL around Wasm calls, and re-acquires GVL for Ruby host callbacks.
ext/src/ruby_api/externals.rs Adds gvl: kwarg parsing to Extern#to_func and updates YARD docs accordingly.
ext/src/helpers/nogvl.rs Implements panic-safe trampolines plus with_gvl to re-enter the GVL from nogvl contexts.
ext/src/helpers/mod.rs Re-exports with_gvl alongside nogvl.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ext/src/helpers/nogvl.rs
Comment on lines +20 to +22
let data = unsafe { &mut *(arg as *mut (Option<F>, MaybeUninit<thread::Result<R>>)) };
let func = data.0.take().expect("closure called more than once");
data.1.write(panic::catch_unwind(AssertUnwindSafe(func)));
Comment thread spec/unit/func_spec.rb
Comment on lines +191 to +205
counter = 0
running = true
sibling = Thread.new { counter += 1 while running }

marks = []
store = Store.new(engine)
mark = Func.new(store, [], []) { marks << counter }
linker = Linker.new(engine)
linker.define(store, "env", "mark", mark)
instance = linker.instantiate(store, mod)

instance.export("spin").to_func(gvl: false).call(500_000_000)

running = false
sibling.join
Comment thread spec/unit/func_spec.rb
linker.define(store, "env", "mark", mark)
instance = linker.instantiate(store, mod)

instance.export("spin").to_func(gvl: false).call(500_000_000)
Comment thread ext/src/ruby_api/func.rs
Comment on lines +297 to +299
// Borrow `ty` so the `move` closure captures a reference, not the owned value.
let ty = &ty;
with_gvl(move || {
Copy link
Copy Markdown
Member

@saulecabrera saulecabrera left a comment

Choose a reason for hiding this comment

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

Currently we have ~4 nogvl uses, all them involve CPU bound tasks, in which there are no callbacks to Ruby. I understand the motivation behind this change, but I think this is inherently unsafe and as such, I expect the documentation around it to reflect that clearly e.g., one case that I think that is not covered in the current state is the interaction between Wasmtime and Ruby through Wasmtime's API:

  • A store declared with store = Store.new(engine, limits: {memory_size: 1_000_000})
  • Will end-up calling Wasmtime's resource limiter
  • Which in turn will call rb_gc_adjust_memory_usage

According to the docs for rb_gc_adjust_memory_usage:

/**
 * Informs that  there are  external memory  usages.  Our GC  runs when  we are
 * running out of memory.  The amount of memory, however, can increase/decrease
 * behind-the-scene.  For  instance DLLs can allocate  memories using `mmap(2)`
 * etc, which  are opaque to  us.  Registering such external  allocations using
 * this function enables  proper detection of how much memories  an object used
 * as a whole.  That will trigger GCs  more often than it would otherwise.  You
 * can  also  pass  negative  numbers  here, to  indicate  that  such  external
 * allocations are gone.

It seems to me that the GVL is needed in that case since GC could mutate the VM state?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants