diff options
author | Samuel Williams <[email protected]> | 2022-12-01 23:00:33 +1300 |
---|---|---|
committer | GitHub <[email protected]> | 2022-12-01 23:00:33 +1300 |
commit | 0436f1e15a8e79ffef5ea412ac1312cbf9f063e6 (patch) | |
tree | 456611e5ae13d2c1312a61532ab7a79d642564b6 | |
parent | 9869bd1d612b489df806cf95bcb56965a02424e0 (diff) |
Introduce `Fiber#storage` for inheritable fiber-scoped variables. (#6612)
Notes
Notes:
Merged-By: ioquatix <[email protected]>
-rw-r--r-- | NEWS.md | 40 | ||||
-rw-r--r-- | cont.c | 314 | ||||
-rw-r--r-- | enumerator.c | 3 | ||||
-rw-r--r-- | include/ruby/internal/intern/cont.h | 21 | ||||
-rw-r--r-- | internal/cont.h | 3 | ||||
-rw-r--r-- | mjit_c.rb | 1 | ||||
-rw-r--r-- | test/fiber/test_storage.rb | 101 | ||||
-rw-r--r-- | thread.c | 2 | ||||
-rw-r--r-- | vm.c | 8 | ||||
-rw-r--r-- | vm_core.h | 5 |
10 files changed, 442 insertions, 56 deletions
@@ -102,6 +102,45 @@ Note that each entry is kept to a minimum, see links for details. Note: We're only listing outstanding class updates. +* Fiber + + * Introduce `Fiber.[]` and `Fiber.[]=` for inheritable fiber storage. + Introduce `Fiber#storage` and `Fiber#storage=` (experimental) for getting + and resetting the current storage. Introduce `Fiber.new(storage:)` for + setting the storage when creating a fiber. [[Feature #19078]] + + Existing Thread and Fiber local variables can be tricky to use. Thread + local variables are shared between all fibers, making it hard to isolate, + while Fiber local variables can be hard to share. It is often desirable + to define unit of execution ("execution context") such that some state + is shared between all fibers and threads created in that context. This is + what Fiber storage provides. + + ```ruby + def log(message) + puts "#{Fiber[:request_id]}: #{message}" + end + + def handle_requests + while request = read_request + Fiber.schedule do + Fiber[:request_id] = SecureRandom.uuid + + request.messages.each do |message| + Fiber.schedule do + log("Handling #{message}") # Log includes inherited request_id. + end + end + end + end + end + ``` + + You should generally consider Fiber storage for any state which you want + to be shared implicitly between all fibers and threads created in a given + context, e.g. a connection pool, a request id, a logger level, + environment variables, configuration, etc. + * Fiber::Scheduler * Introduce `Fiber::Scheduler#io_select` for non-blocking IO.select. @@ -555,3 +594,4 @@ The following deprecated APIs are removed. [Bug #19100]: https://bugs.ruby-lang.org/issues/19100 [Feature #19135]: https://bugs.ruby-lang.org/issues/19135 [Feature #19138]: https://bugs.ruby-lang.org/issues/19138 +[Feature #19078]: https://bugs.ruby-lang.org/issues/19078 @@ -271,7 +271,7 @@ struct rb_fiber_struct { static struct fiber_pool shared_fiber_pool = {NULL, NULL, 0, 0, 0, 0}; -static ID fiber_initialize_keywords[2] = {0}; +static ID fiber_initialize_keywords[3] = {0}; /* * FreeBSD require a first (i.e. addr) argument of mmap(2) is not NULL @@ -1156,7 +1156,9 @@ fiber_memsize(const void *ptr) */ if (saved_ec->local_storage && fiber != th->root_fiber) { size += rb_id_table_memsize(saved_ec->local_storage); + size += rb_obj_memsize_of(saved_ec->storage); } + size += cont_memsize(&fiber->cont); return size; } @@ -2007,11 +2009,186 @@ fiber_t_alloc(VALUE fiber_value, unsigned int blocking) return fiber; } +static rb_fiber_t * +root_fiber_alloc(rb_thread_t *th) +{ + VALUE fiber_value = fiber_alloc(rb_cFiber); + rb_fiber_t *fiber = th->ec->fiber_ptr; + + VM_ASSERT(DATA_PTR(fiber_value) == NULL); + VM_ASSERT(fiber->cont.type == FIBER_CONTEXT); + VM_ASSERT(FIBER_RESUMED_P(fiber)); + + th->root_fiber = fiber; + DATA_PTR(fiber_value) = fiber; + fiber->cont.self = fiber_value; + + coroutine_initialize_main(&fiber->context); + + return fiber; +} + +static inline rb_fiber_t* +fiber_current(void) +{ + rb_execution_context_t *ec = GET_EC(); + if (ec->fiber_ptr->cont.self == 0) { + root_fiber_alloc(rb_ec_thread_ptr(ec)); + } + return ec->fiber_ptr; +} + +static inline VALUE +current_fiber_storage(void) +{ + rb_execution_context_t *ec = GET_EC(); + return ec->storage; +} + +static inline VALUE +inherit_fiber_storage(void) +{ + return rb_obj_dup(current_fiber_storage()); +} + +static inline void +fiber_storage_set(struct rb_fiber_struct *fiber, VALUE storage) +{ + fiber->cont.saved_ec.storage = storage; +} + +static inline VALUE +fiber_storage_get(rb_fiber_t *fiber) +{ + VALUE storage = fiber->cont.saved_ec.storage; + if (storage == Qnil) { + storage = rb_hash_new(); + fiber_storage_set(fiber, storage); + } + return storage; +} + +/** + * call-seq: Fiber.current.storage -> hash (dup) + * + * Returns a copy of the storage hash for the current fiber. + */ +static VALUE +rb_fiber_storage_get(VALUE self) +{ + return rb_obj_dup(fiber_storage_get(fiber_ptr(self))); +} + +static int +fiber_storage_validate_each(VALUE key, VALUE value, VALUE _argument) +{ + rb_check_id(&key); + + return ST_CONTINUE; +} + +static void +fiber_storage_validate(VALUE value) +{ + if (!RB_TYPE_P(value, T_HASH)) { + rb_raise(rb_eTypeError, "storage must be a hash"); + } + + rb_hash_foreach(value, fiber_storage_validate_each, Qundef); +} + +/** + * call-seq: Fiber.current.storage = hash + * + * Sets the storage hash for the current fiber. This feature is experimental + * and may change in the future. + * + * You should be careful about using this method as you may inadvertently clear + * important fiber-storage state. You should mostly prefer to assign specific + * keys in the storage using Fiber#[]=. + * + * You can also use Fiber.new(storage: nil) to create a fiber with an empty + * storage. + * + * Example: + * + * while request = request_queue.pop + * # Reset the per-request state: + * Fiber.current.storage = nil + * handle_request(request) + * end + */ static VALUE -fiber_initialize(VALUE self, VALUE proc, struct fiber_pool * fiber_pool, unsigned int blocking) +rb_fiber_storage_set(VALUE self, VALUE value) { + fiber_storage_validate(value); + + fiber_ptr(self)->cont.saved_ec.storage = rb_obj_dup(value); + return value; +} + +/** + * call-seq: Fiber[key] -> value + * + * Returns the value of the fiber-local variable identified by +key+. + * + * The +key+ must be a symbol, and the value is set by Fiber#[]= or + * Fiber#store. + * + * See also Fiber[]=. + */ +static VALUE +rb_fiber_storage_aref(VALUE class, VALUE key) +{ + ID id = rb_check_id(&key); + if (!id) return Qnil; + + VALUE storage = fiber_storage_get(fiber_current()); + + if (storage == Qnil) return Qnil; + + return rb_hash_aref(storage, key); +} + +/** + * call-seq: Fiber[key] = value + * + * Assign +value+ to the fiber-local variable identified by +key+. + * The variable is created if it doesn't exist. + * + * +key+ must be a Symbol, otherwise a TypeError is raised. + * + * See also Fiber[]. + */ +static VALUE +rb_fiber_storage_aset(VALUE class, VALUE key, VALUE value) +{ + ID id = rb_check_id(&key); + if (!id) return Qnil; + + VALUE storage = fiber_storage_get(fiber_current()); + + return rb_hash_aset(storage, key, value); +} + +static VALUE +fiber_initialize(VALUE self, VALUE proc, struct fiber_pool * fiber_pool, unsigned int blocking, VALUE storage) +{ + if (storage == Qundef || storage == Qtrue) { + // The default, inherit storage (dup) from the current fiber: + storage = inherit_fiber_storage(); + } + else if (storage == Qfalse) { + storage = current_fiber_storage(); + } + else /* nil, hash, etc. */ { + fiber_storage_validate(storage); + storage = rb_obj_dup(storage); + } + rb_fiber_t *fiber = fiber_t_alloc(self, blocking); + fiber->cont.saved_ec.storage = storage; fiber->first_proc = proc; fiber->stack.base = NULL; fiber->stack.pool = fiber_pool; @@ -2044,19 +2221,27 @@ rb_fiber_pool_default(VALUE pool) return &shared_fiber_pool; } +VALUE rb_fiber_inherit_storage(struct rb_execution_context_struct *ec, struct rb_fiber_struct *fiber) +{ + VALUE storage = rb_obj_dup(ec->storage); + fiber->cont.saved_ec.storage = storage; + return storage; +} + /* :nodoc: */ static VALUE rb_fiber_initialize_kw(int argc, VALUE* argv, VALUE self, int kw_splat) { VALUE pool = Qnil; VALUE blocking = Qfalse; + VALUE storage = Qundef; if (kw_splat != RB_NO_KEYWORDS) { VALUE options = Qnil; - VALUE arguments[2] = {Qundef}; + VALUE arguments[3] = {Qundef}; argc = rb_scan_args_kw(kw_splat, argc, argv, ":", &options); - rb_get_kwargs(options, fiber_initialize_keywords, 0, 2, arguments); + rb_get_kwargs(options, fiber_initialize_keywords, 0, 3, arguments); if (!UNDEF_P(arguments[0])) { blocking = arguments[0]; @@ -2065,33 +2250,73 @@ rb_fiber_initialize_kw(int argc, VALUE* argv, VALUE self, int kw_splat) if (!UNDEF_P(arguments[1])) { pool = arguments[1]; } + + storage = arguments[2]; } - return fiber_initialize(self, rb_block_proc(), rb_fiber_pool_default(pool), RTEST(blocking)); + return fiber_initialize(self, rb_block_proc(), rb_fiber_pool_default(pool), RTEST(blocking), storage); } /* * call-seq: - * Fiber.new(blocking: false) { |*args| ... } -> fiber + * Fiber.new(blocking: false, storage: true) { |*args| ... } -> fiber * - * Creates new Fiber. Initially, the fiber is not running and can be resumed with - * #resume. Arguments to the first #resume call will be passed to the block: + * Creates new Fiber. Initially, the fiber is not running and can be resumed + * with #resume. Arguments to the first #resume call will be passed to the + * block: * - * f = Fiber.new do |initial| - * current = initial - * loop do - * puts "current: #{current.inspect}" - * current = Fiber.yield - * end - * end - * f.resume(100) # prints: current: 100 - * f.resume(1, 2, 3) # prints: current: [1, 2, 3] - * f.resume # prints: current: nil - * # ... and so on ... - * - * If <tt>blocking: false</tt> is passed to <tt>Fiber.new</tt>, _and_ current thread - * has a Fiber.scheduler defined, the Fiber becomes non-blocking (see "Non-blocking - * Fibers" section in class docs). + * f = Fiber.new do |initial| + * current = initial + * loop do + * puts "current: #{current.inspect}" + * current = Fiber.yield + * end + * end + * f.resume(100) # prints: current: 100 + * f.resume(1, 2, 3) # prints: current: [1, 2, 3] + * f.resume # prints: current: nil + * # ... and so on ... + * + * If <tt>blocking: false</tt> is passed to <tt>Fiber.new</tt>, _and_ current + * thread has a Fiber.scheduler defined, the Fiber becomes non-blocking (see + * "Non-blocking Fibers" section in class docs). + * + * If the <tt>storage</tt> is unspecified, the default is to inherit a copy of + * the storage from the current fiber. This is the same as specifying + * <tt>storage: true</tt>. + * + * Fiber[:x] = 1 + * Fiber.new do + * Fiber[:x] # => 1 + * Fiber[:x] = 2 + * end.resume + * Fiber[:x] # => 1 + * + * If the <tt>storage</tt> is <tt>false</tt>, this function uses the current + * fiber's storage by reference. This is used for Enumerator to create + * hidden fiber. + * + * Fiber[:count] = 0 + * enumerator = Enumerator.new do |y| + * loop{y << (Fiber[:count] += 1)} + * end + * Fiber[:count] # => 0 + * enumerator.next # => 1 + * Fiber[:count] # => 1 + * + * If the given <tt>storage</tt> is <tt>nil</tt>, this function will lazy + * initialize the internal storage, which starts as an empty hash. + * + * Fiber[:x] = "Hello World" + * Fiber.new(storage: nil) do + * Fiber[:x] # nil + * end + * + * Otherwise, the given <tt>storage</tt> is used as the new fiber's storage, + * and it must be an instance of Hash. + * + * Explicitly using `storage: true/false` is currently experimental and may + * change in the future. */ static VALUE rb_fiber_initialize(int argc, VALUE* argv, VALUE self) @@ -2100,9 +2325,15 @@ rb_fiber_initialize(int argc, VALUE* argv, VALUE self) } VALUE +rb_fiber_new_storage(rb_block_call_func_t func, VALUE obj, VALUE storage) +{ + return fiber_initialize(fiber_alloc(rb_cFiber), rb_proc_new(func, obj), rb_fiber_pool_default(Qnil), 1, storage); +} + +VALUE rb_fiber_new(rb_block_call_func_t func, VALUE obj) { - return fiber_initialize(fiber_alloc(rb_cFiber), rb_proc_new(func, obj), rb_fiber_pool_default(Qnil), 1); + return rb_fiber_new_storage(func, obj, Qtrue); } static VALUE @@ -2276,25 +2507,6 @@ rb_fiber_start(rb_fiber_t *fiber) rb_fiber_terminate(fiber, need_interrupt, err); } -static rb_fiber_t * -root_fiber_alloc(rb_thread_t *th) -{ - VALUE fiber_value = fiber_alloc(rb_cFiber); - rb_fiber_t *fiber = th->ec->fiber_ptr; - - VM_ASSERT(DATA_PTR(fiber_value) == NULL); - VM_ASSERT(fiber->cont.type == FIBER_CONTEXT); - VM_ASSERT(FIBER_RESUMED_P(fiber)); - - th->root_fiber = fiber; - DATA_PTR(fiber_value) = fiber; - fiber->cont.self = fiber_value; - - coroutine_initialize_main(&fiber->context); - - return fiber; -} - // Set up a "root fiber", which is the fiber that every Ractor has. void rb_threadptr_root_fiber_setup(rb_thread_t *th) @@ -2349,16 +2561,6 @@ rb_threadptr_root_fiber_terminate(rb_thread_t *th) } static inline rb_fiber_t* -fiber_current(void) -{ - rb_execution_context_t *ec = GET_EC(); - if (ec->fiber_ptr->cont.self == 0) { - root_fiber_alloc(rb_ec_thread_ptr(ec)); - } - return ec->fiber_ptr; -} - -static inline rb_fiber_t* return_fiber(bool terminate) { rb_fiber_t *fiber = fiber_current(); @@ -3146,6 +3348,7 @@ Init_Cont(void) fiber_initialize_keywords[0] = rb_intern_const("blocking"); fiber_initialize_keywords[1] = rb_intern_const("pool"); + fiber_initialize_keywords[2] = rb_intern_const("storage"); const char *fiber_shared_fiber_pool_free_stacks = getenv("RUBY_SHARED_FIBER_POOL_FREE_STACKS"); if (fiber_shared_fiber_pool_free_stacks) { @@ -3158,8 +3361,13 @@ Init_Cont(void) rb_define_singleton_method(rb_cFiber, "yield", rb_fiber_s_yield, -1); rb_define_singleton_method(rb_cFiber, "current", rb_fiber_s_current, 0); rb_define_singleton_method(rb_cFiber, "blocking", rb_fiber_blocking, 0); + rb_define_singleton_method(rb_cFiber, "[]", rb_fiber_storage_aref, 1); + rb_define_singleton_method(rb_cFiber, "[]=", rb_fiber_storage_aset, 2); + rb_define_method(rb_cFiber, "initialize", rb_fiber_initialize, -1); rb_define_method(rb_cFiber, "blocking?", rb_fiber_blocking_p, 0); + rb_define_method(rb_cFiber, "storage", rb_fiber_storage_get, 0); + rb_define_method(rb_cFiber, "storage=", rb_fiber_storage_set, 1); rb_define_method(rb_cFiber, "resume", rb_fiber_m_resume, -1); rb_define_method(rb_cFiber, "raise", rb_fiber_m_raise, -1); rb_define_method(rb_cFiber, "backtrace", rb_fiber_backtrace, -1); diff --git a/enumerator.c b/enumerator.c index 4d2d73240e..a0c51b585c 100644 --- a/enumerator.c +++ b/enumerator.c @@ -766,7 +766,8 @@ next_init(VALUE obj, struct enumerator *e) { VALUE curr = rb_fiber_current(); e->dst = curr; - e->fib = rb_fiber_new(next_i, obj); + // We inherit the fiber storage by reference, not by copy, by specifying Qfalse here. + e->fib = rb_fiber_new_storage(next_i, obj, Qfalse); e->lookahead = Qundef; } diff --git a/include/ruby/internal/intern/cont.h b/include/ruby/internal/intern/cont.h index 37493009f5..3ee8b08dee 100644 --- a/include/ruby/internal/intern/cont.h +++ b/include/ruby/internal/intern/cont.h @@ -39,6 +39,27 @@ RBIMPL_SYMBOL_EXPORT_BEGIN() VALUE rb_fiber_new(rb_block_call_func_t func, VALUE callback_obj); /** + * Creates a Fiber instance from a C-backended block with the specified storage. + * + * If the given storage is Qundef, this function is equivalent to + * rb_fiber_new() which inherits storage from the current fiber. + * + * If the given storage is Qfalse, this function uses the current fiber's + * storage by reference. + * + * If the given storage is Qnil, this function will lazy initialize the + * internal storage which starts of empty (without any inheritance). + * + * Otherwise, the given storage is used as the internal storage. + * + * @param[in] func A function, to become the fiber's body. + * @param[in] callback_obj Passed as-is to `func`. + * @return An allocated new instance of rb_cFiber, which is ready to be + * "resume"d. + */ +VALUE rb_fiber_new_storage(rb_block_call_func_t func, VALUE callback_obj, VALUE storage); + +/** * Queries the fiber which is calling this function. Any ruby execution * context has its fiber, either explicitly or implicitly. * diff --git a/internal/cont.h b/internal/cont.h index acac0254d3..c3b091668a 100644 --- a/internal/cont.h +++ b/internal/cont.h @@ -22,6 +22,9 @@ void rb_jit_cont_init(void); void rb_jit_cont_each_iseq(rb_iseq_callback callback, void *data); void rb_jit_cont_finish(void); +// Copy locals from the current execution to the specified fiber. +VALUE rb_fiber_inherit_storage(struct rb_execution_context_struct *ec, struct rb_fiber_struct *fiber); + VALUE rb_fiberptr_self(struct rb_fiber_struct *fiber); unsigned int rb_fiberptr_blocking(struct rb_fiber_struct *fiber); struct rb_execution_context_struct * rb_fiberptr_get_ec(struct rb_fiber_struct *fiber); @@ -442,6 +442,7 @@ module RubyVM::MJIT local_storage: [CType::Pointer.new { self.rb_id_table }, Primitive.cexpr!("OFFSETOF((*((struct rb_execution_context_struct *)NULL)), local_storage)")], local_storage_recursive_hash: [self.VALUE, Primitive.cexpr!("OFFSETOF((*((struct rb_execution_context_struct *)NULL)), local_storage_recursive_hash)")], local_storage_recursive_hash_for_trace: [self.VALUE, Primitive.cexpr!("OFFSETOF((*((struct rb_execution_context_struct *)NULL)), local_storage_recursive_hash_for_trace)")], + storage: [self.VALUE, Primitive.cexpr!("OFFSETOF((*((struct rb_execution_context_struct *)NULL)), storage)")], root_lep: [CType::Pointer.new { self.VALUE }, Primitive.cexpr!("OFFSETOF((*((struct rb_execution_context_struct *)NULL)), root_lep)")], root_svar: [self.VALUE, Primitive.cexpr!("OFFSETOF((*((struct rb_execution_context_struct *)NULL)), root_svar)")], ensure_list: [CType::Pointer.new { self.rb_ensure_list_t }, Primitive.cexpr!("OFFSETOF((*((struct rb_execution_context_struct *)NULL)), ensure_list)")], diff --git a/test/fiber/test_storage.rb b/test/fiber/test_storage.rb new file mode 100644 index 0000000000..0a6942617c --- /dev/null +++ b/test/fiber/test_storage.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true +require 'test/unit' + +class TestFiberStorage < Test::Unit::TestCase + def test_storage + Fiber.new do + Fiber[:x] = 10 + assert_kind_of Hash, Fiber.current.storage + assert_predicate Fiber.current.storage, :any? + end.resume + end + + def test_storage_inherited + Fiber.new do + Fiber[:foo] = :bar + + Fiber.new do + assert_equal :bar, Fiber[:foo] + Fiber[:bar] = :baz + end.resume + + assert_nil Fiber[:bar] + end.resume + end + + def test_variable_assignment + Fiber.new do + Fiber[:foo] = :bar + assert_equal :bar, Fiber[:foo] + end.resume + end + + def test_storage_assignment + Fiber.new do + Fiber.current.storage = {foo: :bar} + assert_equal :bar, Fiber[:foo] + end.resume + end + + def test_inherited_storage + Fiber.new do + Fiber.current.storage = {foo: :bar} + f = Fiber.new do + assert_equal :bar, Fiber[:foo] + end + f.resume + end.resume + end + + def test_enumerator_inherited_storage + Fiber.new do + Fiber[:item] = "Hello World" + + enumerator = Enumerator.new do |out| + out << Fiber.current + out << Fiber[:item] + end + + # The fiber within the enumerator is not equal to the current... + assert_not_equal Fiber.current, enumerator.next + + # But it inherited the storage from the current fiber: + assert_equal "Hello World", enumerator.next + end.resume + end + + def test_thread_inherited_storage + Fiber.new do + Fiber[:x] = 10 + + x = Thread.new do + Fiber[:y] = 20 + Fiber[:x] + end.value + + assert_equal 10, x + assert_equal nil, Fiber[:y] + end.resume + end + + def test_enumerator_count + Fiber.new do + Fiber[:count] = 0 + + enumerator = Enumerator.new do |y| + # Since the fiber is implementation detail, the storage are shared with the parent: + Fiber[:count] += 1 + y << Fiber[:count] + end + + assert_equal 1, enumerator.next + assert_equal 1, Fiber[:count] + end.resume + end + + def test_storage_assignment_type_error + assert_raise(TypeError) do + Fiber.new(storage: {Object.new => "bar"}) {} + end + end +end @@ -813,6 +813,8 @@ thread_create_core(VALUE thval, struct thread_create_params *params) "can't start a new thread (frozen ThreadGroup)"); } + rb_fiber_inherit_storage(ec, th->ec->fiber_ptr); + switch (params->type) { case thread_invoke_type_proc: th->invoke_type = thread_invoke_type_proc; @@ -3047,7 +3047,7 @@ vm_init2(rb_vm_t *vm) } void -rb_execution_context_update(const rb_execution_context_t *ec) +rb_execution_context_update(rb_execution_context_t *ec) { /* update VM stack */ if (ec->vm_stack) { @@ -3087,6 +3087,8 @@ rb_execution_context_update(const rb_execution_context_t *ec) cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp); } } + + ec->storage = rb_gc_location(ec->storage); } static enum rb_id_table_iterator_result @@ -3154,6 +3156,8 @@ rb_execution_context_mark(const rb_execution_context_t *ec) RUBY_MARK_UNLESS_NULL(ec->local_storage_recursive_hash); RUBY_MARK_UNLESS_NULL(ec->local_storage_recursive_hash_for_trace); RUBY_MARK_UNLESS_NULL(ec->private_const_reference); + + RUBY_MARK_MOVABLE_UNLESS_NULL(ec->storage); } void rb_fiber_mark_self(rb_fiber_t *fib); @@ -3344,6 +3348,8 @@ th_init(rb_thread_t *th, VALUE self, rb_vm_t *vm) th->ec->local_storage_recursive_hash = Qnil; th->ec->local_storage_recursive_hash_for_trace = Qnil; + th->ec->storage = Qnil; + #if OPT_CALL_THREADED_CODE th->retval = Qundef; #endif @@ -968,6 +968,9 @@ struct rb_execution_context_struct { VALUE local_storage_recursive_hash; VALUE local_storage_recursive_hash_for_trace; + /* Inheritable fiber storage. */ + VALUE storage; + /* eval env */ const VALUE *root_lep; VALUE root_svar; @@ -2010,7 +2013,7 @@ void rb_threadptr_pending_interrupt_clear(rb_thread_t *th); void rb_threadptr_pending_interrupt_enque(rb_thread_t *th, VALUE v); VALUE rb_ec_get_errinfo(const rb_execution_context_t *ec); void rb_ec_error_print(rb_execution_context_t * volatile ec, volatile VALUE errinfo); -void rb_execution_context_update(const rb_execution_context_t *ec); +void rb_execution_context_update(rb_execution_context_t *ec); void rb_execution_context_mark(const rb_execution_context_t *ec); void rb_fiber_close(rb_fiber_t *fib); void Init_native_thread(rb_thread_t *th); |