mruby/mruby

Procs passed to Fibers via C throw "double resume" error if `Fiber.yield` is called from a loop

Closed this issue · 2 comments

I am currently working on a project, where I try to embed mruby fibers into another programming language.
While doing so, I encountered a weird phenomenon (here broken down to a simple C example).

The following code is supposed to copy a Proc as argument to a new Fiber object and then execute the Fiber a few times.

C source file:

#include <mruby.h>
#include <mruby/class.h>
#include <mruby/compile.h>

int main() {
  mrb_state *mrb = mrb_open();
  FILE* file = fopen("script.rb", "r");
  mrb_load_file(mrb, file);
  if(file) fclose(file);

  mrb_value proc_value = mrb_load_string(mrb, "$proc");
  mrb_value test_class = mrb_obj_value(mrb_class_get(mrb, "Test"));
  mrb_sym method_sym = mrb_intern(mrb, "fiber_from", 10);
  mrb_value fiber_value = mrb_funcall_argv(mrb, test_class, method_sym, 1, &proc_value);

  for(int i = 0; i < 5; i++) {
    mrb_sym resume_method_sym = mrb_intern(mrb, "resume", 6); 

    // Relevant function call is here
    mrb_funcall_argv(mrb, fiber_value, resume_method_sym, 0, NULL);

    if(mrb->exc) mrb_print_error(mrb);
  }

  mrb_close(mrb);
  return 0;
}

Ruby script file:

# script.rb

class Test
  def self.fiber_from(proc)
    Fiber.new(&proc)
  end
end

$proc = Proc.new do
  0.upto(5) do |i|
    puts i
    Fiber.yield
    puts 10 + i
  end
end

When compiling and executing the C file, I get the following results:

0
10
trace (most recent call last):
        [1] (unknown):0
(unknown):0:in resume: double resume (FiberError)
trace (most recent call last):
        [1] (unknown):0
(unknown):0:in resume: double resume (FiberError)
trace (most recent call last):
        [1] (unknown):0
(unknown):0:in resume: double resume (FiberError)

However, if I replace the Proc constructor argument with the following code, everything works fine:

$proc = Proc.new do
  puts 1
  Fiber.yield
  puts 2
  Fiber.yield
  puts 3
  Fiber.yield
  puts 4
  Fiber.yield
  puts 5
end

Result:

1
2
3
4
5

Other loop instructions like loop and while show the same problem, so I'd guess that the issue lies somewhere with the loop block. Also the problem does not occur until the end of the loop block is reached for the first time.

The issue does also not arise when I call Test.fiber_from and the following resume calls from the script directly, so the detour over C seems to be relevant here.

If I'm missing or misunderstanding some essential things here, please let me know.

Further information:

  • mruby version: 3.1.0
  • Platform: Windows 10 64bit
  • Compiler: Visual Studio 2019
matz commented

The latter was working rather accidentally. In fact, the latest version gives you the following errors for both version of script.rb.

trace (most recent call last):
	[1] (unknown):0
(unknown):0:in resume: can't cross C function boundary (FiberError)
trace (most recent call last):
	[1] (unknown):0
(unknown):0:in resume: can't cross C function boundary (FiberError)
trace (most recent call last):
	[1] (unknown):0
(unknown):0:in resume: can't cross C function boundary (FiberError)
trace (most recent call last):
	[1] (unknown):0
(unknown):0:in resume: can't cross C function boundary (FiberError)
trace (most recent call last):
	[1] (unknown):0
(unknown):0:in resume: can't cross C function boundary (FiberError)

The point is C function boundary. Since mruby fibers are only swap VM stacks (and not C stack), so when resume is called from different C stack position, it raises an error. You used mrb_funcall to call resume but since mrb_funcall itself is a C function, you got function boundary error.

To fix this, use API to resume the fiber, replace

mrb_funcall_argv(mrb, fiber_value, resume_method_sym, 0, NULL);

by

mrb_fiber_resume(mrb, fiber_value, 0, NULL);

I believe it would work (at least this simplified version worked).

Thank you very much, that did indeed solve it!