diff --git a/include/mruby.h b/include/mruby.h index 7a533cfa7..3ed86f8c7 100644 --- a/include/mruby.h +++ b/include/mruby.h @@ -1458,8 +1458,6 @@ MRB_API mrb_value mrb_fiber_new(mrb_state *mrb, const struct RProc *proc); * Implemented in mruby-fiber * * Switches to the specified fiber and executes. Like the `Fiber#resume` method. - * - * @note It can only be called before entering the mruby VM (e.g. in the `main()` function). */ MRB_API mrb_value mrb_fiber_resume(mrb_state *mrb, mrb_value fib, mrb_int argc, const mrb_value *argv); diff --git a/mrbgems/mruby-fiber/src/fiber.c b/mrbgems/mruby-fiber/src/fiber.c index e91415dfe..4deb0d6a4 100644 --- a/mrbgems/mruby-fiber/src/fiber.c +++ b/mrbgems/mruby-fiber/src/fiber.c @@ -170,6 +170,17 @@ fiber_check_cfunc(mrb_state *mrb, struct mrb_context *c) } } +static void +fiber_check_cfunc_recursive(mrb_state *mrb, struct mrb_context *c) +{ + for (;; c = c->prev) { + fiber_check_cfunc(mrb, c); + if (c == mrb->root_c || !c->prev) { + break; + } + } +} + static void fiber_switch_context(mrb_state *mrb, struct mrb_context *c) { @@ -275,14 +286,17 @@ fiber_switch(mrb_state *mrb, mrb_value self, mrb_int len, const mrb_value *a, mr else { value = fiber_result(mrb, a, len); if (vmexec) { + if (c->ci > c->cibase) c->ci--; /* pop dummy callinfo */ c->ci[1].stack[0] = value; } } if (vmexec) { + int cci = old_c->ci->cci; c->vmexec = TRUE; value = mrb_vm_exec(mrb, c->ci->proc, c->ci->pc); mrb->c = old_c; + old_c->ci->cci = cci; /* restore values as they may have changed in Fiber.yield */ } else { MARK_CONTEXT_MODIFY(c); @@ -304,19 +318,19 @@ fiber_switch(mrb_state *mrb, mrb_value self, mrb_int len, const mrb_value *a, mr * to the next Fiber.yield statement inside the fiber's block * or to the block value if it runs to completion without any * Fiber.yield - * - * This method cannot be called from C using mrb_funcall(). - * Use mrb_fiber_resume() function instead. */ static mrb_value fiber_resume(mrb_state *mrb, mrb_value self) { const mrb_value *a; mrb_int len; + mrb_bool vmexec = FALSE; - fiber_check_cfunc(mrb, mrb->c); mrb_get_args(mrb, "*!", &a, &len); - return fiber_switch(mrb, self, len, a, TRUE, FALSE); + if (mrb->c->ci->cci > 0) { + vmexec = TRUE; + } + return fiber_switch(mrb, self, len, a, TRUE, vmexec); } MRB_API mrb_value @@ -429,7 +443,7 @@ fiber_transfer(mrb_state *mrb, mrb_value self) const mrb_value* a; mrb_int len; - fiber_check_cfunc(mrb, mrb->c); + fiber_check_cfunc_recursive(mrb, mrb->c); mrb_get_args(mrb, "*!", &a, &len); if (c->status == MRB_FIBER_RESUMED) { @@ -472,7 +486,6 @@ mrb_fiber_yield(mrb_state *mrb, mrb_int len, const mrb_value *a) if (c->vmexec) { c->vmexec = FALSE; mrb->c->ci->cci = CINFO_RESUMED; - c->ci--; /* pop callinfo for yield */ } MARK_CONTEXT_MODIFY(mrb->c); return fiber_result(mrb, a, len); diff --git a/mrbgems/mruby-fiber/test/fiber.rb b/mrbgems/mruby-fiber/test/fiber.rb index 64b9d8175..ea541acf3 100644 --- a/mrbgems/mruby-fiber/test/fiber.rb +++ b/mrbgems/mruby-fiber/test/fiber.rb @@ -1,204 +1,210 @@ -assert('Fiber.new') do - f = Fiber.new{} - assert_kind_of Fiber, f -end +begin + $fiber_test_activity = __FILE__ -assert('Fiber#resume') do - f = Fiber.new{|x| x } - assert_equal 2, f.resume(2) -end - -assert('Fiber#transfer') do - ary = [] - f2 = nil - f1 = Fiber.new{ - ary << f2.transfer(:foo) - :ok - } - f2 = Fiber.new{ - ary << f1.transfer(:baz) - :ng - } - assert_equal(:ok, f1.transfer) - assert_equal([:baz], ary) - assert_false f1.alive? -end - -assert('Fiber#alive?') do - f = Fiber.new{ Fiber.yield } - f.resume - assert_true f.alive? - f.resume - assert_false f.alive? -end - -assert('Fiber#==') do - root = Fiber.current - assert_equal root, root - assert_equal root, Fiber.current - assert_false root != Fiber.current - f = Fiber.new { - assert_false root == Fiber.current - } - f.resume - assert_false f == root - assert_true f != root -end - -assert('Fiber.yield') do - f = Fiber.new{|x| Fiber.yield x } - assert_equal 3, f.resume(3) - assert_true f.alive? -end - -assert('FiberError') do - assert_equal StandardError, FiberError.superclass -end - -assert('Fiber iteration') do - f1 = Fiber.new{ - [1,2,3].each{|x| Fiber.yield(x)} - } - f2 = Fiber.new{ - [9,8,7].each{|x| Fiber.yield(x)} - } - a = [] - 3.times { - a << f1.resume - a << f2.resume - } - assert_equal [1,9,2,8,3,7], a -end - -assert('Fiber with splat in the block argument list') { - assert_equal([1], Fiber.new{|*x|x}.resume(1)) -} - -assert('Fiber raises on resume when dead') do - assert_raise(FiberError) do + assert('Fiber.new') do f = Fiber.new{} + assert_kind_of Fiber, f + end + + assert('Fiber#resume') do + f = Fiber.new{|x| x } + assert_equal 2, f.resume(2) + end + + assert('Fiber#transfer') do + ary = [] + f2 = nil + f1 = Fiber.new{ + ary << f2.transfer(:foo) + :ok + } + f2 = Fiber.new{ + ary << f1.transfer(:baz) + :ng + } + assert_equal(:ok, f1.transfer) + assert_equal([:baz], ary) + assert_false f1.alive? + end + + assert('Fiber#alive?') do + f = Fiber.new{ Fiber.yield } + f.resume + assert_true f.alive? f.resume assert_false f.alive? - f.resume end -end -assert('Yield raises when called on root fiber') do - assert_raise(FiberError) { Fiber.yield } -end + assert('Fiber#==') do + root = Fiber.current + assert_equal root, root + assert_equal root, Fiber.current + assert_false root != Fiber.current + f = Fiber.new { + assert_false root == Fiber.current + } + f.resume + assert_false f == root + assert_true f != root + end -assert('Double resume of Fiber') do - f1 = Fiber.new {} - f2 = Fiber.new { - f1.resume - assert_raise(FiberError) { f2.resume } - Fiber.yield 0 + assert('Fiber.yield') do + f = Fiber.new{|x| Fiber.yield x } + assert_equal 3, f.resume(3) + assert_true f.alive? + end + + assert('FiberError') do + assert_equal StandardError, FiberError.superclass + end + + assert('Fiber iteration') do + f1 = Fiber.new{ + [1,2,3].each{|x| Fiber.yield(x)} + } + f2 = Fiber.new{ + [9,8,7].each{|x| Fiber.yield(x)} + } + a = [] + 3.times { + a << f1.resume + a << f2.resume + } + assert_equal [1,9,2,8,3,7], a + end + + assert('Fiber with splat in the block argument list') { + assert_equal([1], Fiber.new{|*x|x}.resume(1)) } - assert_equal 0, f2.resume - f2.resume - assert_false f1.alive? - assert_false f2.alive? -end -assert('Recursive resume of Fiber') do - f1, f2 = nil, nil - f1 = Fiber.new { assert_raise(FiberError) { f2.resume } } - f2 = Fiber.new { - f1.resume - Fiber.yield 0 - } - f3 = Fiber.new { + assert('Fiber raises on resume when dead') do + assert_raise(FiberError) do + f = Fiber.new{} + f.resume + assert_false f.alive? + f.resume + end + end + + assert('Yield raises when called on root fiber') do + assert_raise(FiberError) { Fiber.yield } + end + + assert('Double resume of Fiber') do + f1 = Fiber.new {} + f2 = Fiber.new { + f1.resume + assert_raise(FiberError) { f2.resume } + Fiber.yield 0 + } + assert_equal 0, f2.resume f2.resume - } - assert_equal 0, f3.resume - f2.resume - assert_false f1.alive? - assert_false f2.alive? - assert_false f3.alive? -end + assert_false f1.alive? + assert_false f2.alive? + end -assert('Root fiber resume') do - root = Fiber.current - assert_raise(FiberError) { root.resume } - f = Fiber.new { + assert('Recursive resume of Fiber') do + f1, f2 = nil, nil + f1 = Fiber.new { assert_raise(FiberError) { f2.resume } } + f2 = Fiber.new { + f1.resume + Fiber.yield 0 + } + f3 = Fiber.new { + f2.resume + } + assert_equal 0, f3.resume + f2.resume + assert_false f1.alive? + assert_false f2.alive? + assert_false f3.alive? + end + + assert('Root fiber resume') do + root = Fiber.current assert_raise(FiberError) { root.resume } - } - f.resume - assert_false f.alive? -end - -assert('Fiber without block') do - assert_raise(ArgumentError) { Fiber.new } -end - - -assert('Transfer to self.') do - result = [] - f = Fiber.new { result << :start; f.transfer; result << :end } - f.transfer - assert_equal [:start, :end], result - - result = [] - f = Fiber.new { result << :start; f.transfer; result << :end } - f.resume - assert_equal [:start, :end], result -end - -assert('Resume transferred fiber') do - f = Fiber.new { - assert_raise(FiberError) { f.resume } - } - f.transfer -end - -assert('Root fiber transfer.') do - result = nil - root = Fiber.current - f = Fiber.new { - result = :ok - root.transfer - } - f.transfer - assert_true f.alive? - assert_equal :ok, result -end - -assert('Break nested fiber with root fiber transfer') do - root = Fiber.current - - result = nil - f2 = nil - f1 = Fiber.new { - root.transfer(f2.transfer) - result = :f1 - } - f2 = Fiber.new { - result = :to_root - root.transfer :from_f2 - result = :f2 - } - assert_equal :from_f2, f1.transfer - assert_equal :to_root, result - assert_equal :f2, f2.transfer - assert_equal :f2, result - assert_false f2.alive? - assert_equal nil, f1.transfer - assert_equal :f1, f1.transfer - assert_equal :f1, result - assert_false f1.alive? -end - -assert('CRuby Fiber#transfer test.') do - ary = [] - f2 = nil - f1 = Fiber.new{ - ary << f2.transfer(:foo) - :ok - } - f2 = Fiber.new{ - ary << f1.transfer(:baz) - :ng - } - assert_equal :ok, f1.transfer - assert_equal [:baz], ary + f = Fiber.new { + assert_raise(FiberError) { root.resume } + } + f.resume + assert_false f.alive? + end + + assert('Fiber without block') do + assert_raise(ArgumentError) { Fiber.new } + end + + + assert('Transfer to self.') do + result = [] + f = Fiber.new { result << :start; f.transfer; result << :end } + f.transfer + assert_equal [:start, :end], result + + result = [] + f = Fiber.new { result << :start; f.transfer; result << :end } + f.resume + assert_equal [:start, :end], result + end + + assert('Resume transferred fiber') do + f = Fiber.new { + assert_raise(FiberError) { f.resume } + } + f.transfer + end + + assert('Root fiber transfer.') do + result = nil + root = Fiber.current + f = Fiber.new { + result = :ok + root.transfer + } + f.transfer + assert_true f.alive? + assert_equal :ok, result + end + + assert('Break nested fiber with root fiber transfer') do + root = Fiber.current + + result = nil + f2 = nil + f1 = Fiber.new { + root.transfer(f2.transfer) + result = :f1 + } + f2 = Fiber.new { + result = :to_root + root.transfer :from_f2 + result = :f2 + } + assert_equal :from_f2, f1.transfer + assert_equal :to_root, result + assert_equal :f2, f2.transfer + assert_equal :f2, result + assert_false f2.alive? + assert_equal nil, f1.transfer + assert_equal :f1, f1.transfer + assert_equal :f1, result + assert_false f1.alive? + end + + assert('CRuby Fiber#transfer test.') do + ary = [] + f2 = nil + f1 = Fiber.new{ + ary << f2.transfer(:foo) + :ok + } + f2 = Fiber.new{ + ary << f1.transfer(:baz) + :ng + } + assert_equal :ok, f1.transfer + assert_equal [:baz], ary + end +ensure + $fiber_test_activity = nil end diff --git a/mrbgems/mruby-fiber/test/fiber2.rb b/mrbgems/mruby-fiber/test/fiber2.rb new file mode 100644 index 000000000..a6ff8a791 --- /dev/null +++ b/mrbgems/mruby-fiber/test/fiber2.rb @@ -0,0 +1,155 @@ +# This file tests fiber switching crossing C functions + +unless RUBY_ENGINE == "mruby" + class Fiber + alias resume_by_c_func resume + alias resume_by_c_method resume + + class << self + alias yield_by_c_func yield + + def yield_by_c_method(*args) + raise FiberError, "ycan't cross C function boundary" + end + end + end + + def Proc.c_tunnel + yield + end +end + +begin + $fiber_test_activity = __FILE__ + + assert('Call Fiber#resume nested with C') do + assert_equal "ok1", Fiber.new { Fiber.new { "ok1" }.resume_by_c_func }.resume_by_c_func + assert_equal "ok2", Fiber.new { Fiber.new { "ok2" }.resume_by_c_method }.resume_by_c_func + assert_equal "ok3", Fiber.new { Fiber.new { "ok3" }.resume_by_c_func }.resume_by_c_method + assert_equal "ok4", Fiber.new { Fiber.new { "ok4" }.resume_by_c_method }.resume_by_c_method + assert_equal "ok5", Fiber.new { Proc.c_tunnel { Fiber.new { "ok5" }.resume_by_c_func } }.resume_by_c_func + assert_equal "ok6", Fiber.new { Proc.c_tunnel { Fiber.new { "ok6" }.resume_by_c_method } }.resume_by_c_func + assert_equal "ok7", Fiber.new { Proc.c_tunnel { Fiber.new { "ok7" }.resume_by_c_func } }.resume_by_c_method + assert_equal "ok8", Fiber.new { Proc.c_tunnel { Fiber.new { "ok8" }.resume_by_c_method } }.resume_by_c_method + assert_equal "ok9", Fiber.new { Proc.c_tunnel { Fiber.new { "ok9" }.resume } }.resume_by_c_func + assert_equal "ok10", Fiber.new { Proc.c_tunnel { Fiber.new { "ok10" }.resume } }.resume_by_c_method + end + + assert('Call Fiber#resume and Fiber.yield mixed with C.') do + assert_equal 1, Fiber.new { Fiber.yield 1 }.resume_by_c_func + assert_equal 2, Fiber.new { Fiber.yield 2 }.resume_by_c_method + assert_equal 3, Fiber.new { Fiber.yield_by_c_func 3 }.resume + assert_equal 4, Fiber.new { Fiber.yield_by_c_func 4 }.resume_by_c_func + assert_equal 5, Fiber.new { Fiber.yield_by_c_func 5 }.resume_by_c_method + assert_raise(FiberError) { Fiber.new { Fiber.yield_by_c_method "bad" }.resume } + assert_raise(FiberError) { Fiber.new { Fiber.yield_by_c_method "bad" }.resume_by_c_func } + assert_raise(FiberError) { Fiber.new { Fiber.yield_by_c_method "bad" }.resume_by_c_method } + + result = [] + f1 = Fiber.new { result << Fiber.new { Fiber.yield 1; "bad" }.resume_by_c_func; 2 } + f2 = Fiber.new { result << f1.resume; 3 } + result << f2.resume + assert_equal [1, 2, 3], result + + f1 = Fiber.new { + -> { + Fiber.yield 1 + Fiber.yield_by_c_func 2 + f2 = Fiber.new { + -> { + Fiber.yield_by_c_func 3 + Fiber.yield 4 + Fiber.yield_by_c_func 5 + Fiber.yield 6 + }.call + 7 + } + Fiber.yield f2.resume_by_c_func + Fiber.yield f2.resume + Fiber.yield f2.resume_by_c_method + Fiber.yield f2.resume + Fiber.yield f2.resume_by_c_func + Fiber.yield 8 + }.call + Fiber.yield 9 + 10 + } + result = [] + 10.times { result << f1.resume } + assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], result + end + + assert('Call Fiber#resume and Fiber.yield mixed with C and raising exceptions') do + f = Fiber.new do + raise ZeroDivisionError + rescue + Fiber.yield "rescue" + "pass1" + ensure + Fiber.yield "ensure" + end + assert_equal "rescue", f.resume_by_c_method + assert_equal "ensure", f.resume_by_c_method + assert_equal "pass1", f.resume_by_c_method + assert_raise(FiberError) { f.resume_by_c_method } + + f = Fiber.new do + raise ZeroDivisionError + rescue + Fiber.yield "rescue" + "pass2" + ensure + Fiber.yield "ensure" + end + assert_equal "rescue", f.resume_by_c_func + assert_equal "ensure", f.resume_by_c_func + assert_equal "pass2", f.resume_by_c_func + assert_raise(FiberError) { f.resume_by_c_func } + + f2 = Fiber.new do + -> do + Fiber.yield 1 + raise "3" + ensure + Fiber.yield 2 + end.call + "NOT REACH 1" + end + f1 = Fiber.new do + Fiber.yield f2.resume_by_c_func + begin + Fiber.yield f2.resume + Fiber.yield f2.resume_by_c_method + Fiber.yield "NOT REACH 2" + rescue => e + Fiber.yield e.message + Fiber.yield 4 + ensure + Fiber.yield 5 + end + Fiber.yield 6 + 7 + end + result = [] + 7.times { result << f1.resume } + assert_equal [1, 2, "3", 4, 5, 6, 7], result + end + + assert('Call Fiber#transfer with C') do + assert_equal "ok1", Fiber.new { Fiber.new { "ok1" }.resume_by_c_method }.transfer + assert_equal "ok2", Fiber.new { Fiber.new { "ok2" }.resume_by_c_func }.transfer + assert_raise(FiberError) { Proc.c_tunnel { Fiber.new { "BAD!" }.transfer } } + + b = Fiber.current + a = Fiber.new { + Proc.c_tunnel { + Fiber.new { + b.transfer + }.resume + } + } + assert_raise(FiberError) { a.transfer } + end +ensure + $fiber_test_activity = nil +end diff --git a/mrbgems/mruby-fiber/test/fibertest.c b/mrbgems/mruby-fiber/test/fibertest.c new file mode 100644 index 000000000..69680aecb --- /dev/null +++ b/mrbgems/mruby-fiber/test/fibertest.c @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include + +static mrb_value +fiber_s_yield_by_c_func(mrb_state *mrb, mrb_value self) +{ + mrb_value a = mrb_get_arg1(mrb); + return mrb_fiber_yield(mrb, 1, &a); +} + +static mrb_value +fiber_s_yield_by_c_method(mrb_state *mrb, mrb_value self) +{ + mrb_value a = mrb_get_arg1(mrb); + return mrb_funcall_argv(mrb, self, mrb_intern_lit(mrb, "yield"), 1, &a); +} + +static mrb_value +fiber_resume_by_c_func(mrb_state *mrb, mrb_value self) +{ + int ci_index = mrb->c->ci - mrb->c->cibase; + mrb_value ret = mrb_fiber_resume(mrb, self, 0, NULL); + if (ci_index != mrb->c->ci - mrb->c->cibase) { + mrb_raisef(mrb, E_EXCEPTION, + "[BUG] INVALID CI POSITION (expected %d, but actual %d) [BUG]", + (int)ci_index, (int)(mrb->c->ci - mrb->c->cibase)); + } + return ret; +} + +static mrb_value +fiber_resume_by_c_method(mrb_state *mrb, mrb_value self) +{ + int ci_index = mrb->c->ci - mrb->c->cibase; + mrb_value ret = mrb_funcall_argv(mrb, self, mrb_intern_lit(mrb, "resume"), 0, NULL); + if (ci_index != mrb->c->ci - mrb->c->cibase) { + mrb_raisef(mrb, E_EXCEPTION, + "[BUG] INVALID CI POSITION (expected %d, but actual %d) [BUG]", + (int)ci_index, (int)(mrb->c->ci - mrb->c->cibase)); + } + return ret; +} + +static mrb_value +fiber_transfer_by_c(mrb_state *mrb, mrb_value self) +{ + return mrb_funcall_argv(mrb, self, mrb_intern_lit(mrb, "transfer"), 0, NULL); +} + +static mrb_value +proc_s_c_tunnel(mrb_state *mrb, mrb_value self) +{ + mrb_value b; + mrb_get_args(mrb, "&!", &b); + return mrb_yield_argv(mrb, b, 0, NULL); +} + +static void +check_activity(mrb_state *mrb) +{ + mrb_value act = mrb_gv_get(mrb, mrb_intern_lit(mrb, "$fiber_test_activity")); + if (mrb_test(act)) { + act = mrb_obj_as_string(mrb, act); + fprintf(stderr, "\n\t<<<%s%.*s>>>\n", + "mruby VM has an unexpected outage in ", (int)RSTRING_LEN(act), RSTRING_PTR(act)); + abort(); + } +} + +void +mrb_mruby_fiber_gem_test(mrb_state *mrb) +{ + struct RClass *fiber_class = mrb_class_get(mrb, "Fiber"); + mrb_define_class_method(mrb, fiber_class, "yield_by_c_func", fiber_s_yield_by_c_func, MRB_ARGS_ANY()); + mrb_define_class_method(mrb, fiber_class, "yield_by_c_method", fiber_s_yield_by_c_method, MRB_ARGS_ANY()); + mrb_define_method(mrb, fiber_class, "resume_by_c_func", fiber_resume_by_c_func, MRB_ARGS_NONE()); + mrb_define_method(mrb, fiber_class, "resume_by_c_method", fiber_resume_by_c_method, MRB_ARGS_NONE()); + mrb_define_method(mrb, fiber_class, "transfer_by_c", fiber_transfer_by_c, MRB_ARGS_NONE()); + + mrb_define_class_method(mrb, mrb->proc_class, "c_tunnel", proc_s_c_tunnel, MRB_ARGS_NONE() | MRB_ARGS_BLOCK()); + + mrb_gv_set(mrb, mrb_intern_lit(mrb, "$fiber_test_activity"), mrb_nil_value()); + mrb_state_atexit(mrb, check_activity); +} diff --git a/src/vm.c b/src/vm.c index 2d2a919c4..35e1650f9 100644 --- a/src/vm.c +++ b/src/vm.c @@ -1839,6 +1839,7 @@ RETRY_TRY_BLOCK: } recv = MRB_METHOD_FUNC(m)(mrb, recv); } + mrb_assert(mrb->c->ci > mrb->c->cibase); mrb_gc_arena_shrink(mrb, ai); if (mrb->exc) goto L_RAISE; ci = mrb->c->ci; @@ -2245,7 +2246,10 @@ RETRY_TRY_BLOCK: mrb->c = c->prev; if (!mrb->c) mrb->c = mrb->root_c; else c->prev = NULL; - goto L_RAISE; + if (!c->vmexec) goto L_RAISE; + mrb->jmp = prev_jmp; + if (!prev_jmp) return mrb_obj_value(mrb->exc); + MRB_THROW(prev_jmp); } }