diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 648c642bd7ccbb..246d0c8722a0c1 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -98,7 +98,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@7b8d4719ee4aaa279bdf55df38dacb9ebfe12a6c # v2.87.6 + - uses: taiki-e/install-action@84f5ac3124727fb3d284d4d22ee9ab3654fd09a6 # v2.87.7 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 15427f8b8a5130..9cb401f8a7f2ea 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -152,7 +152,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@7b8d4719ee4aaa279bdf55df38dacb9ebfe12a6c # v2.87.6 + - uses: taiki-e/install-action@84f5ac3124727fb3d284d4d22ee9ab3654fd09a6 # v2.87.7 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/NEWS.md b/NEWS.md index f969edbd136ce2..86ddf609d18d45 100644 --- a/NEWS.md +++ b/NEWS.md @@ -177,9 +177,9 @@ They are still available on rubygems.org and can be installed with ### The following default gems are updated. -* RubyGems 4.1.0.dev +* RubyGems 4.1.0.beta1 * 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12], [v4.0.13][RubyGems-v4.0.13], [v4.0.14][RubyGems-v4.0.14], [v4.0.15][RubyGems-v4.0.15], [v4.0.16][RubyGems-v4.0.16], [v4.0.17][RubyGems-v4.0.17], [v4.0.18][RubyGems-v4.0.18], [v4.0.19][RubyGems-v4.0.19], [v4.0.20][RubyGems-v4.0.20] -* bundler 4.1.0.dev +* bundler 4.1.0.beta1 * 4.0.3 to [v4.0.4][bundler-v4.0.4], [v4.0.5][bundler-v4.0.5], [v4.0.6][bundler-v4.0.6], [v4.0.7][bundler-v4.0.7], [v4.0.8][bundler-v4.0.8], [v4.0.9][bundler-v4.0.9], [v4.0.10][bundler-v4.0.10], [v4.0.11][bundler-v4.0.11], [v4.0.12][bundler-v4.0.12], [v4.0.13][bundler-v4.0.13], [v4.0.14][bundler-v4.0.14], [v4.0.15][bundler-v4.0.15], [v4.0.16][bundler-v4.0.16], [v4.0.17][bundler-v4.0.17] * erb 6.0.7 * 6.0.1 to [v6.0.1.1][erb-v6.0.1.1], [v6.0.2][erb-v6.0.2], [v6.0.3][erb-v6.0.3], [v6.0.4][erb-v6.0.4], [v6.0.5][erb-v6.0.5], [v6.0.6][erb-v6.0.6], [v6.0.7][erb-v6.0.7] @@ -188,7 +188,7 @@ They are still available on rubygems.org and can be installed with * 0.8.2 to [v0.9.0][io-console-v0.9.0], [v0.9.1][io-console-v0.9.1], [v0.9.2][io-console-v0.9.2] * ipaddr 1.2.9 * 1.2.8 to [v1.2.9][ipaddr-v1.2.9] -* json 3.0.1 +* json 3.0.2 * 2.18.0 to [v2.18.1][json-v2.18.1], [v2.19.0][json-v2.19.0], [v2.19.1][json-v2.19.1], [v2.19.2][json-v2.19.2], [v2.19.3][json-v2.19.3], [v2.19.4][json-v2.19.4], [v2.19.5][json-v2.19.5], [v2.19.6][json-v2.19.6], [v2.19.7][json-v2.19.7], [v2.19.8][json-v2.19.8], [v2.19.9][json-v2.19.9], [v2.20.0][json-v2.20.0], [v2.21.0][json-v2.21.0], [v2.21.2][json-v2.21.2], [v3.0.0.rc1][json-v3.0.0.rc1], [v3.0.0][json-v3.0.0] * net-protocol 0.3.0 * 0.2.2 to [v0.3.0][net-protocol-v0.3.0] diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index c674ebc2bae9b9..e263f34f09ac66 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -2863,3 +2863,31 @@ def st.m; :strct end 6.times { GC.start } [taken.map(&:receive), [closed.receive, closed.receive]] } + +# Closing a port wakes a receiver waiting on it, with or without a timeout. +assert_equal '[:closed, :closed]', %q{ + untimed = Ractor::Port.new + th = Thread.new do + begin + untimed.receive + rescue Ractor::ClosedError + :closed + end + end + Thread.pass until th.status == 'sleep' + untimed.close + untimed_result = th.value # before the next close, which would wake it too + + timed = Ractor::Port.new + th2 = Thread.new do + begin + timed.receive(timeout: 10) + rescue Ractor::ClosedError + :closed + end + end + Thread.pass until th2.status == 'sleep' + timed.close + + [untimed_result, th2.value] +} diff --git a/box.c b/box.c index d58082e15bb37e..72b4b932225af9 100644 --- a/box.c +++ b/box.c @@ -203,21 +203,21 @@ rb_box_gc_update_references(void *ptr) if (!box) return; if (box->box_object) - box->box_object = rb_gc_location(box->box_object); + rb_gc_update_moved(&box->box_object); if (box->top_self) - box->top_self = rb_gc_location(box->top_self); - box->load_path = rb_gc_location(box->load_path); - box->expanded_load_path = rb_gc_location(box->expanded_load_path); - box->load_path_snapshot = rb_gc_location(box->load_path_snapshot); + rb_gc_update_moved(&box->top_self); + rb_gc_update_moved(&box->load_path); + rb_gc_update_moved(&box->expanded_load_path); + rb_gc_update_moved(&box->load_path_snapshot); if (box->load_path_check_cache) { - box->load_path_check_cache = rb_gc_location(box->load_path_check_cache); + rb_gc_update_moved(&box->load_path_check_cache); } - box->loaded_features = rb_gc_location(box->loaded_features); - box->loaded_features_snapshot = rb_gc_location(box->loaded_features_snapshot); - box->loaded_features_realpaths = rb_gc_location(box->loaded_features_realpaths); - box->loaded_features_realpath_map = rb_gc_location(box->loaded_features_realpath_map); - box->ruby_dln_libmap = rb_gc_location(box->ruby_dln_libmap); - box->gvar_tbl = rb_gc_location(box->gvar_tbl); + rb_gc_update_moved(&box->loaded_features); + rb_gc_update_moved(&box->loaded_features_snapshot); + rb_gc_update_moved(&box->loaded_features_realpaths); + rb_gc_update_moved(&box->loaded_features_realpath_map); + rb_gc_update_moved(&box->ruby_dln_libmap); + rb_gc_update_moved(&box->gvar_tbl); } void diff --git a/common.mk b/common.mk index 4fbd986118dbf6..e6559c6ca41160 100644 --- a/common.mk +++ b/common.mk @@ -410,7 +410,7 @@ program: $(SHOWFLAGS) $(DOT_WAIT) $(PROGRAM) wprogram: $(SHOWFLAGS) $(DOT_WAIT) $(WPROGRAM) mini: PHONY miniruby$(EXEEXT) -$(PROGRAM) $(WPROGRAM): $(LIBRUBY) $(MAINOBJ) $(OBJS) $(EXTOBJS) $(SETUP) $(PREP) +$(PROGRAM) $(WPROGRAM): $(LIBRUBY) $(MAINOBJ) $(OBJS) $(EXTOBJS) $(SETUP) $(PREP) $(PROGRAM_EXTS) $(LIBRUBY_A): $(LIBRUBY_A_OBJS) $(MAINOBJ) $(INITOBJS) $(ARCHFILE) diff --git a/configure.ac b/configure.ac index fdb92a6f04d9fe..b09d549f76a7b8 100644 --- a/configure.ac +++ b/configure.ac @@ -1237,6 +1237,7 @@ main() AS_IF([test -n "$dsymutil"], [ POSTLINK="$dsymutil \$@ 2>/dev/null${POSTLINK:+; $POSTLINK}" ]) + : ${POSTLINK:=:} AC_CHECK_HEADERS(crt_externs.h, [], [], [ #include ]) @@ -3177,15 +3178,15 @@ AC_ARG_WITH(static-linked-ext, [AS_CASE([$withval],[yes],[STATIC=;EXTSTATIC=static],[no],[],[EXTSTATIC="$withval"])]) AS_CASE([",$EXTSTATIC,"], [,static,|*,enc,*], [ ENCOBJS='enc/encinit.$(OBJEXT) enc/libenc.$(LIBEXT) enc/libtrans.$(LIBEXT)' - EXTOBJS='ext/extinit.$(OBJEXT)' AC_DEFINE_UNQUOTED(EXTSTATIC, 1) AC_SUBST(ENCSTATIC, static) ], [ ENCOBJS='dmyenc.$(OBJEXT)' - EXTOBJS='dmyext.$(OBJEXT)' ]) +EXTOBJS='dmyext.$(OBJEXT)' AC_SUBST(ENCOBJS) AC_SUBST(EXTOBJS) +AC_SUBST(PROGRAM_EXTS, [${EXTSTATIC:+exts}]) : "rpath" && { AS_CASE(["$target_os"], diff --git a/cont.c b/cont.c index 7d3aba3093d145..0d0f00f60e0f18 100644 --- a/cont.c +++ b/cont.c @@ -1107,9 +1107,9 @@ cont_compact(void *ptr) rb_context_t *cont = ptr; if (cont->self) { - cont->self = rb_gc_location(cont->self); + rb_gc_update_moved(&cont->self); } - cont->value = rb_gc_location(cont->value); + rb_gc_update_moved(&cont->value); rb_execution_context_update(&cont->saved_ec); } @@ -1220,7 +1220,7 @@ void rb_fiber_update_self(rb_fiber_t *fiber) { if (fiber->cont.self) { - fiber->cont.self = rb_gc_location(fiber->cont.self); + rb_gc_update_moved(&fiber->cont.self); } else { rb_execution_context_update(&fiber->cont.saved_ec); @@ -1237,7 +1237,7 @@ static void fiber_compact(void *ptr) { rb_fiber_t *fiber = ptr; - fiber->first_proc = rb_gc_location(fiber->first_proc); + rb_gc_update_moved(&fiber->first_proc); if (fiber->prev) rb_fiber_update_self(fiber->prev); @@ -2124,10 +2124,18 @@ static const rb_data_type_t rb_fiber_data_type = { 0, 0, RUBY_TYPED_FREE_IMMEDIATELY }; +static VALUE fiber_alloc_in(VALUE klass, void *objspace); + static VALUE fiber_alloc(VALUE klass) { - VALUE obj = TypedData_Wrap_Struct(klass, &rb_fiber_data_type, 0); + return fiber_alloc_in(klass, GET_RACTOR()->objspace); +} + +static VALUE +fiber_alloc_in(VALUE klass, void *objspace) +{ + VALUE obj = rb_data_typed_object_wrap_in_objspace(objspace, klass, 0, &rb_fiber_data_type); rb_gc_declare_weak_references(obj); return obj; } @@ -2701,10 +2709,10 @@ rb_threadptr_root_fiber_setup(rb_thread_t *th) } void -rb_root_fiber_obj_setup(rb_thread_t *th) +rb_root_fiber_obj_setup(rb_thread_t *th, void *objspace) { rb_fiber_t *fiber = th->ec->fiber_ptr; - VALUE fiber_value = fiber_alloc(rb_cFiber); + VALUE fiber_value = fiber_alloc_in(rb_cFiber, objspace); DATA_PTR(fiber_value) = fiber; fiber->cont.self = fiber_value; } diff --git a/doc/contributing/efficient_extensions.md b/doc/contributing/efficient_extensions.md new file mode 100644 index 00000000000000..78027b5806e6b6 --- /dev/null +++ b/doc/contributing/efficient_extensions.md @@ -0,0 +1,487 @@ +# Tips for updating Ruby C extensions for efficiently using Ruby 4 VM APIs + +## Why? + +The Ruby VM has evolved a lot from Ruby 2 to Ruby 4. While this evolution was reflected in Ruby's C APIs, great care was taken to provide backwards compatibility. Old gems work with little change on Ruby 4, ensuring apps don't get stuck on old versions just because some dependency has not been updated. + +Yet, C extensions that keep using old APIs and design patterns are leaving performance and safety advances "on the table" not only for the extensions themselves, but also silently disabling or hindering major Ruby VM optimizations, getting in the way of GC, Ractors, observability, etc. + +As much as possible, C extensions should have "mechanical sympathy" for the Ruby VM. +When C extensions use old APIs or do things incorrectly, Ruby needs to fall back to safer behaviors, which means the whole application runs more slowly because of those C extensions. + +## The "elephant" in the room + +### Tip: 🟧 _Avoid native extensions if you can_ + +Can you avoid doing the thing in native code at all? See and . Can you use [ffi](https://github.com/ffi/ffi) instead? + +## Arrays + +### Tip: 🟥 _Never use `RARRAY_PTR`_ + +Impact: _Slows down GC forever (while the object lives)_ + +Why: +1. It "wb unprotects" the object forever. Aka object is permanently in young generation, GC always needs to scan it forever even if it's not changing +2. The impact applies even if you only read from the array once; Ruby doesn't know it and is conservative forever +3. You can accidentally write to a frozen array +4. No bounds checking, you can accidentally corrupt memory + +Do instead: +1. Use `rb_ary_entry` to read +2. Use `rb_ary_store` to write +3. If an API requires a contiguous `VALUE` pointer, use `RARRAY_PTR_USE` to keep raw-pointer access scoped + +### Tip: 🟧 _Avoid using `RARRAY_AREF` to read an array_ + +Impact: _No bounds checking (unsafe)_ + +Why: +1. Using `RARRAY_AREF(array, 12345)` will always try to read, even if array is not that long, potentially leading to subtle bugs +2. If you call back into Ruby code while operating on an array, it's possible a different thread will get to run and update the array, so the length may change unexpectedly + +Do instead: +1. Use `rb_ary_entry`, it checks the array is long enough to contain the entry requested + +### Tip: 🟩 _Use `rb_ary_store` to write to an array_ + +Impact: _GC faster, and respects frozen_ + +Why: +1. It checks if an array is frozen before writing to it +2. It does not "write barrier unprotect" the object +3. It correctly implements the (very cheap) write barrier only if needed -- and this only affects the next GC, not all GC forever + +## Hashes + +### Tip: 🟥 _Never use `RHASH_TBL`_ + +Impact: _Slows down GC forever (while the object lives), no bounds/frozen checking_ + +Why: +1. It "wb unprotects" the object forever. Aka object is permanently in young generation, GC always needs to scan it forever even if it's not changing +2. The impact applies even if you only read from the hash once; Ruby doesn't know it and is conservative forever + +(You might recognize the points above as "same downsides as `RARRAY_PTR`", but for hashes) + +3. Forces "ar table" to "st table" conversion => Ruby has a special compact representation for small hashes and this forces the non-compact representation always + +Do instead: +* Use `rb_hash_...` functions + +## Strings + +### Tip: 🟩 _Use fstrings for repeated/long-lived strings_ + +Impact: _Lower app memory use, faster hash lookups, slightly slower creation_ + +Quick detour: What are fstrings? +* TL;DR Global deduplicated frozen strings +* Same as `String#dedup`/`String#-@` but directly from C + +``` +VALUE v1 = rb_interned_str_cstr("hello"); +VALUE v2 = rb_interned_str_cstr("hello"); +``` + +`v1 == v2` in C (same object!) + +(Very similar behavior to symbols!) + +Why: +1. It lowers memory use since only one copy of string data is kept +2. It speeds up hash inserts with string keys, since a non-frozen String key gets fstring-deduplicated on insert anyway -- passing an fstring avoids that extra work + +## Memory + +### Tip: 🟩 _Use `ruby_xmalloc`/`ruby_xfree`/`ruby_x`... to manage memory_ + +Impact: _Improved low memory handling, improved GC behavior, safer_ + +Why: +1. Ruby will automatically GC if the app runs out of memory to try to recover +2. Ruby will trigger GC after some amount of allocations to avoid fragmentation and high memory usage +3. No need to error check -- it always returns memory or raises an exception directly +4. Maybe MMTk will be able to optimize it further in the future? ;) + +(mmtk.c) + +```c +// Malloc +void * +rb_gc_impl_malloc(void *objspace_ptr, size_t size, bool gc_allowed) +{ + // TODO: don't use system malloc + return malloc(size); +} +``` + +### Tip: 🟧 _Be careful about making objects "immortal"_ + +Impact: _Higher memory use_ + +Why: +1. APIs such as `rb_gc_register_mark_object` and `rb_define_class`/`rb_define_module` cause objects to become immortal -- they can never be garbage collected nor can the garbage collector move them during compaction + +Do instead (*sometimes?): +1. `rb_global_variable` prevents an object referenced from being garbage collected and moved, but once the variable stops pointing at the object the effect disappears + +### Tip: 🟩 _Use sized/bulk APIs to avoid unnecessary resizing of Arrays, Hashes, Strings_ + +When creating Arrays, Hashes, and Strings, you can often provide sizes and perform operations in bulk. + +Look for APIs such as: + +* Arrays: `rb_ary_new_capa(size)` / `rb_ary_new_from_args(size, ...)` / `rb_ary_new_from_values(size, ...)` / `rb_ary_cat(...)` +* Hashes: `rb_hash_new_capa` / `rb_hash_bulk_insert` +* Strings: `rb_str_buf_new(capa)` + +Impact: _Faster performance, reduced memory usage_ + +Why: +1. When Ruby knows the size of objects, it can take advantage of Variable Width Allocation to find a size pool that can contain the entire object, keeping all the data together in memory and saving the overhead of extra allocations +2. When performing mutations in bulk, Ruby can resize the object once to fit all the changes, rather than needing to incrementally grow +3. Calling into Ruby once with all the changes is often faster than calling many times to feed each change (as there are checks that need to be done on every call, for instance) + +## Ractors + +### Tip: 🟩 _If your extension is Ractor-safe, remember to tell Ruby about it with `rb_ext_ractor_safe`_ + +Impact: _Faster performance! (Even very slightly with a single Ractor)_ + +Why: +1. Allow Ruby app to take advantage of true parallelism +2. When an extension doesn't declare itself Ractor-safe, Ruby wraps every method call with the equivalent of + +```c +VALUE call_cfunc(...) { + if (!rb_ractor_main_p()) { + rb_raise(rb_eRactorUnsafeError, "ractor unsafe method called from not main ractor"); + } + return call_original_function(...) +} +``` + +Note: `rb_ext_ractor_safe(true)` must be called **before** the `rb_define_method` calls for the methods you want optimized. +It changes the invoker used for methods defined *afterward*; calling it later does not retrofit already-defined methods. +Whenever possible, consider doing it in the extension's `Init_...` function. + +## TypedData + +What is TypedData? It allows a Ruby object to wrap a C struct or similar native memory. + +```c +struct SimpleSomeStruct { + int internal_info; +}; +VALUE obj = TypedData_Make_Struct(klass, struct SimpleSomeStruct, &some_typed_data_type, ptr); // <-- Wraps struct with Ruby object +``` + +But a TypedData object has one key very powerful feature -- it can hold **references** to Ruby objects. That is, you can use it to implement your own custom instance variables, arrays, hashes, etc that reference Ruby objects. + +```c +struct SomeStruct { + int internal_info; + VALUE some_reference; // <-- Here we can keep a reference to another ruby object! Like a C "instance variable" +}; +VALUE obj = ...; +``` + +BUT holding references to Ruby objects means TypedData must coordinate with the Ruby garbage collector. +And the Ruby garbage collector has evolved quite a bit from Ruby 2 to 4 to get the most performance out of your applications, and this evolution often cannot be hidden/abstracted away from TypedData objects. +TypedData + Ruby references is the ultimate in "mechanical sympathy". + +**Running example.** We'll use this `SomeStruct` as the basis for a `SomeClass` that we'll carry through the rest of the section. + +```c +static void some_struct_mark(void *ptr) { + struct SomeStruct *data = ptr; + rb_gc_mark(data->some_reference); // pins the reference, see tip on `dcompact` below +} + +static const rb_data_type_t some_typed_data_type = { + .wrap_struct_name = "SomeStruct", + .function = { + .dmark = some_struct_mark, + .dfree = RUBY_TYPED_DEFAULT_FREE, // = ruby_xfree + // .dsize omitted -- see tip on `dsize` below + // .dcompact omitted -- see tip on `dcompact` below + }, + // .flags omitted -- see tips below +}; + +static VALUE some_class_set_some_reference(VALUE self, VALUE reference) { + struct SomeStruct *data; + TypedData_Get_Struct(self, struct SomeStruct, &some_typed_data_type, data); + data->some_reference = reference; // no write barrier -- see tip on it below + return reference; +} +``` + +### Tip: 🟧 _Avoid using TypedData, especially for referencing Ruby objects if you can!_ + +Impact: _Simplicity and correctness (sanity?)_ + +Why: +1. As we'll see, implementing all of the TypedData features is quite complex +2. If you can store your extension's data in a regular Ruby array or Ruby hash, consider doing that! Those are extremely optimized already! +3. You'll be able to ignore ALL of the tips that follow! + +### Tip: 🟩 _Use declarative marking_ + +Impact: _Lower memory usage, and easier to maintain_ + +Why: +1. GC compaction enables Ruby to move objects together, improving performance and reducing memory usage (including across forks) +2. Declarative marking avoids having separate mark and compact functions that need to be kept in-sync + +Applied to the running example -- delete the hand-written `some_struct_mark`, declare the offsets, flip the flag: + +```c +RUBY_REFERENCES(some_struct_refs) = { + RUBY_REF_EDGE(struct SomeStruct, some_reference), + RUBY_REF_END, +}; + +static const rb_data_type_t some_typed_data_type = { + .wrap_struct_name = "SomeStruct", + .function = { + .dmark = RUBY_REFS_LIST_PTR(some_struct_refs), // refs go in dmark, not data! + .dfree = RUBY_TYPED_DEFAULT_FREE, + }, + .flags = RUBY_TYPED_DECL_MARKING, +}; +``` + +No `dmark` function, no `dcompact` function -- the GC walks the offset list for both marking and compaction. +If you later add a new `VALUE` member to `struct SomeStruct`, remember to add a matching `RUBY_REF_EDGE` and that's it! + +### Tip: 🟥 _Always provide a dcompact function when referencing Ruby objects_ + +(*UNLESS you're using declarative marking) + +Impact: _Pins referenced objects, preventing them from being compacted_ + +Why: +1. A dcompact allows Ruby to still do GC compaction, even when not using declarative marking +2. Without it, objects referenced by a TypedData object are "pinned" in place, unable to ever move + +Applied to the running example -- change `rb_gc_mark` to `rb_gc_mark_movable` and add a compact callback: + +```c +static void some_struct_mark(void *ptr) { + struct SomeStruct *data = ptr; + rb_gc_mark_movable(data->some_reference); // was rb_gc_mark -> pinned +} + +static void some_struct_compact(void *ptr) { + struct SomeStruct *data = ptr; + VALUE ref = rb_gc_location(data->some_reference); + if (ref != data->some_reference) { // Copy-on-write friendly check, don't rewrite unless it changed + data->some_reference = ref; + } +} + +static const rb_data_type_t some_typed_data_type = { + .wrap_struct_name = "SomeStruct", + .function = { + .dmark = some_struct_mark, + .dfree = RUBY_TYPED_DEFAULT_FREE, + .dcompact = some_struct_compact, // NEW + }, +}; +``` + +`rb_gc_location` returns the (possibly new) address of an object that was marked movable. +If the object wasn't moved, it returns the same `VALUE` unchanged -- so it's always safe to assign back. + +### Tip: 🟥 _Do not use TypedData without `RUBY_TYPED_WB_PROTECTED`_ + +Impact: _Slows down GC_ + +Why: +1. It "wb unprotects" the object forever. Aka object is permanently in young generation, GC always needs to scan it forever even if it's not changing + +Do instead: +1. Declare TypedData as `RUBY_TYPED_WB_PROTECTED` +2. Whenever a reference is written, you must use `RB_OBJ_WRITE` -- don't forget! + +Applied to the running example -- set the flag AND route every `VALUE` store through `RB_OBJ_WRITE`: + +```c +static VALUE some_class_set_some_reference(VALUE self, VALUE reference) { + struct SomeStruct *data; + TypedData_Get_Struct(self, struct SomeStruct, &some_typed_data_type, data); + RB_OBJ_WRITE(self, &data->some_reference, reference); // was: data->some_reference = reference; + return reference; +} + +static const rb_data_type_t some_typed_data_type = { + .wrap_struct_name = "SomeStruct", + .function = { ... }, + .flags = RUBY_TYPED_WB_PROTECTED, // NEW +}; +``` + +### Tip: 🟩 _Implement dsize_ + +Impact: _Accurate memory information_ + +Why: +1. This exposes correct memory accounting to `ObjectSpace` APIs (`memsize_of`, `count_objects_size`, `dump`, `dump_all`) +2. Otherwise Ruby will only count the Ruby size of the object, not the C size + +Do: +1. Write a dsize! ;) + +Applied to the running example -- add a size reporter: + +```c +static size_t some_struct_dsize(const void *ptr) { + return sizeof(struct SomeStruct); +} + +static const rb_data_type_t some_typed_data_type = { + .wrap_struct_name = "SomeStruct", + .function = { + .dmark = some_struct_mark, + .dfree = RUBY_TYPED_DEFAULT_FREE, + .dsize = some_struct_dsize, // NEW + }, + ... +}; +``` + +If `struct SomeStruct` later owns heap-allocated memory (e.g. malloc and the like), include their sizes in the return value too. + +### Tip: 🟧 _If possible, use `RUBY_TYPED_FREE_IMMEDIATELY`_ + +Impact: _Faster garbage collection and earlier memory reclamation_ + +Why: +1. When `RUBY_TYPED_FREE_IMMEDIATELY` is set, you are "promising" to Ruby that it's safe to call dfree immediately, during GC. To make your function safe, you should never call into Ruby APIs (like trying to release the GVL) and ideally avoid any kind of blocking or I/O. A function that just calls xfree/free on things is one example of something free to call immediately. With this "promise", Ruby is able to call the dfree immediately during object sweeping, thus freeing up the memory immediately. +2. Without it -- that is, for a custom `dfree` callback that hasn't opted in -- Ruby keeps the object as a "zombie", then needs to add it to a list of pages to finalize, then needs to do extra work to go through the page, etc... Hard to avoid in some situations. This deferred handling applies specifically to custom `dfree` functions; `RUBY_TYPED_DEFAULT_FREE` (used in our example) is already recognized and safely executed immediately during sweeping regardless of this flag, since Ruby knows in advance that it's just a plain `xfree`. + +Applied to the running example -- our `dfree` is just `ruby_xfree` (never touches the GVL, never blocks), so the flag is safe to add: + +```c +static const rb_data_type_t some_typed_data_type = { + ... + .flags = RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FREE_IMMEDIATELY, // + FREE_IMMEDIATELY +}; +``` + +### Tip: 🟧 _Consider using `RUBY_TYPED_THREAD_SAFE_FREE`_ + +Impact: _Faster GC when multiple Ractors are running_ + +Why: +1. `RUBY_TYPED_THREAD_SAFE_FREE` (new in Ruby 4.1) tells Ruby it's safe to call your `dfree` function from multiple Ractors in parallel, letting Ractor-local GC sweep objects of this type without deferring +2. Without it, once more than one Ractor is running, Ruby doesn't know if it's safe to call `dfree` concurrently, so it defers freeing those objects to a later time +3. Having no `dfree` function, or using `RUBY_TYPED_DEFAULT_FREE` (as in our running example), is already automatically safe to sweep in parallel -- this flag only matters if you have a custom `dfree` + +Do make sure your `dfree` is genuinely thread-safe (doesn't mutate global/shared state) before adding this flag. + +### Tip: 🟧 _Consider using `RUBY_TYPED_EMBEDDABLE`_ + +Impact: _Faster GC, fewer memory allocations, better code performance (trade off with complexity)_ + +Why: +1. By embedding the struct directly inside the Ruby object, we avoid having the classic split of TypedData objects into two parts +2. Which means the second part does not need a separate malloc -> less work AND means the second part does not need a separate free -> less work +3. Which means less pointer chasing by the cpu, and better cache use (reading the object will bring the rest into the cache) +4. Proven track record of improving performance for core types + +But be careful -- see the latest Ruby docs on and the discussion on , especially around the use of `RB_GC_GUARD`, not storing pointers to/into the C structure, how `dfree`/`dsize` need to be changed, and `RUBY_TYPED_FREE_IMMEDIATELY` being required. + +This is kind-of an advanced use case, so I recommend it only as an optimization for objects accessed very often, or created in large numbers. "With great power comes great responsibility" kinda feature. + +As per , this is a Ruby 4.1+ feature "officially", but you can use it as far back as Ruby 3.3... +I think? I didn't find any reason _not_ to use it, do let me know if I missed it. + +Applied to the running example -- `struct SomeStruct` is tiny (one int + one `VALUE`), so it's a strong candidate for embedding directly in the Ruby object slot: + +```c +static size_t some_struct_dsize(const void *ptr) { + return 0; // struct now embedded in the Ruby object slot, so Ruby + // already accounts for its size -- report only auxiliary + // heap allocations here +} + +static const rb_data_type_t some_typed_data_type = { + ... + .function = { + ... + .dsize = some_struct_dsize, // CHANGED: no longer counts sizeof(struct SomeStruct) + }, + .flags = RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE, +}; +``` + +Note that it requires `RUBY_TYPED_FREE_IMMEDIATELY`. + +Also note that `dsize` must be revisited: once the struct is embedded, its memory is already part of the Ruby object slot, so `dsize` returning `sizeof(struct SomeStruct)` (as in the earlier tip) would double-count it in `ObjectSpace` accounting. `dsize` should report only auxiliary heap allocations owned by the struct (e.g. malloc'd buffers) -- zero if there are none, as here. + +### Tip: 🟧 _Consider using mass marking/compacting functions_ + +Ruby provides a number of "mark all/compact all" reference functions: + +* `rb_gc_mark_locations(const VALUE *start, const VALUE *end)` + * Conservatively (maybe) mark + pin everything between two pointers +* `rb_mark_hash(struct st_table *tbl)` + * For an `st_table` with `VALUE` keys and values + * Exact mark + pin every key and value +* `rb_mark_set(struct st_table *tbl)` + * For an `st_table` with `VALUE` keys + * Exact mark + pin every key +* `rb_mark_tbl(struct st_table *tbl)` + * For an `st_table` with `VALUE` values + * Exact mark + pin every value + * (Doesn't do anything with the keys) +* `rb_mark_tbl_no_pin(struct st_table *tbl)` + * For an `st_table` with `VALUE` values + * Exact mark + **does not pin** every value + * (Doesn't do anything with the keys) + * During `dcompact`, you need to update values with `rb_gc_location` + * I advise against using `rb_gc_update_tbl_refs` as the documentation is incorrect -- see + +And there's a few more variants that are available but hidden: +* `rb_gc_mark_values(long n, const VALUE *values)` + * Similar in spirit to `rb_gc_mark_locations` but exact mark + **no pin** + * `rb_gc_update_values(long n, VALUE *values)` should be used in `dcompact` +* `rb_gc_mark_vm_stack_values(long n, const VALUE *values)` + * Similar in spirit to `rb_gc_mark_locations` but exact mark + **pin** + +In practice, there's a combination of: +* Kind/shape of data structure (st_table, begin/end pointers, beginning + length) +* Conservative (maybe) mark vs exact mark +* Pin vs no-pin +* Availability of `dcompact` counterpart + +that aren't available. Would it be useful for gems to support more? + +Impact: _Maybe (?) faster GC, simplification_ + +Why: +1. As with any low-level vs high-level API, it's much better if you can tell Ruby "I want to mark all of this" vs going one by one +2. ...Although Ruby right now maps this to "one-by-one" so... the gain is more on expressiveness than speed (and maybe future MMTk optimization?) + +### Tip: 🟩 _Prefer `TypedData_Make_Struct` over `TypedData_Wrap_Struct`_ + +Impact: _Avoids memory leaks on the error path_ + +Why: +1. `TypedData_Wrap_Struct` takes a struct pointer you allocated yourself. If wrapping raises (e.g. out-of-memory when allocating the Ruby object), the struct leaks. +2. `TypedData_Make_Struct` allocates both the Ruby object and the struct as a single operation -- nothing to clean up on failure. + +Sharp edge: if struct allocation inside `Make_Struct` fails, you can end up with a Ruby object whose data pointer is `NULL`. Still usually better than the leak. Reach for `Wrap_Struct` only when you already have a pre-existing struct pointer you can't recreate. + +### Tip: 🟧 _Set `RUBY_TYPED_FROZEN_SHAREABLE` if you want frozen TypedData to cross Ractors_ + +Impact: _Allows frozen instances to be shared across Ractors without copying_ + +Why: +1. Without the flag, `Ractor.make_shareable(obj)` raises for your type. +2. With the flag (plus freezing + `Ractor.make_shareable`), the object can be read from any Ractor. + +Do make sure that your object and C code are able to correctly run across many Ractors with no thread safety issues. diff --git a/doc/maintainers.md b/doc/maintainers.md index 494716c6dba5e0..1c4ec8896ae722 100644 --- a/doc/maintainers.md +++ b/doc/maintainers.md @@ -148,8 +148,8 @@ consensus on ruby-core/ruby-dev. #### lib/erb.rb -* Masatoshi SEKI ([seki]) * Takashi Kokubun ([k0kubun]) +* Jean Boussier ([byroot]) * https://github.com/ruby/erb * https://rubygems.org/gems/erb diff --git a/error.c b/error.c index 923249ebe56fab..b99183e3cccfbc 100644 --- a/error.c +++ b/error.c @@ -1042,9 +1042,41 @@ bug_report_end(FILE *out, rb_pid_t pid) finish_report(out, pid); } +/* Only the first thread to get here writes a report. A second one -- another Ractor + * failing the same assertion, say -- would interleave into it and leave both + * unreadable [Bug #21146], so it waits for the writer to abort the process instead. + * The writer is let through again, for the crash-while-reporting path. */ +static const rb_execution_context_t *bug_reporter_ec; +static rb_atomic_t bug_reporter_claimed; + +static bool +bug_report_claim(void) +{ + const rb_execution_context_t *ec = rb_current_execution_context(false); + + if (RUBY_ATOMIC_CAS(bug_reporter_claimed, 0, 1) == 0) { + bug_reporter_ec = ec; + return true; + } + if (ec != NULL && ec == bug_reporter_ec) { + return true; + } + + /* Bounded, so a writer that hangs ends as a crash and not as a hang. */ + for (int i = 0; i < 100; i++) { +#ifdef _WIN32 + Sleep(100); +#else + struct timespec ts = { 0, 100 * 1000 * 1000 }; + nanosleep(&ts, NULL); +#endif + } + return false; +} + #define report_bug(file, line, fmt, ctx) do { \ rb_pid_t pid = -1; \ - FILE *out = bug_report_file(file, line, &pid); \ + FILE *out = bug_report_claim() ? bug_report_file(file, line, &pid) : NULL; \ if (out) { \ bug_report_begin(out, fmt); \ rb_vm_bugreport(ctx, out); \ @@ -1054,7 +1086,7 @@ bug_report_end(FILE *out, rb_pid_t pid) #define report_bug_valist(file, line, fmt, ctx, args) do { \ rb_pid_t pid = -1; \ - FILE *out = bug_report_file(file, line, &pid); \ + FILE *out = bug_report_claim() ? bug_report_file(file, line, &pid) : NULL; \ if (out) { \ bug_report_begin_valist(out, fmt, args); \ rb_vm_bugreport(ctx, out); \ @@ -1207,7 +1239,7 @@ rb_assert_failure_detail(const char *file, int line, const char *name, const cha const char *fmt, ...) { rb_pid_t pid = -1; - FILE *out = bug_report_file(file, line, &pid); + FILE *out = bug_report_claim() ? bug_report_file(file, line, &pid) : NULL; if (out) { fputs("Assertion Failed: ", out); if (name) fprintf(out, "%s:", name); diff --git a/ext/-test-/gvl/call_without_gvl/call_without_gvl.c b/ext/-test-/gvl/call_without_gvl/call_without_gvl.c index 97946e925d3059..178b050863d716 100644 --- a/ext/-test-/gvl/call_without_gvl/call_without_gvl.c +++ b/ext/-test-/gvl/call_without_gvl/call_without_gvl.c @@ -68,6 +68,31 @@ thread_ubf_async_safe(VALUE thread, VALUE notify_fd) return Qnil; } +/* Churn the malloc accounting from a thread that released the GVL but kept its EC. + * The block is over GC_MALLOC_INCREASE_LOCAL_THRESHOLD so every free reaches the + * objspace instead of the thread-local counter; freeing it sized keeps that true + * where malloc_usable_size is unavailable. */ +#define XFREE_LOOP_SIZE (16 * 1024) + +static void * +xfree_loop(void *p) +{ + for (long i = *(long *)p; i > 0; i--) { + ruby_xfree_sized(ruby_xmalloc(XFREE_LOOP_SIZE), XFREE_LOOP_SIZE); + } + return NULL; +} + +static VALUE +thread_xfree_without_gvl(VALUE klass, VALUE count) +{ + long n = NUM2LONG(count); + + rb_thread_call_without_gvl(xfree_loop, &n, RUBY_UBF_IO, NULL); + + return Qnil; +} + void Init_call_without_gvl(void) { @@ -75,4 +100,5 @@ Init_call_without_gvl(void) VALUE klass = rb_define_module_under(mBug, "Thread"); rb_define_singleton_method(klass, "runnable_sleep", thread_runnable_sleep, 1); rb_define_singleton_method(klass, "ubf_async_safe", thread_ubf_async_safe, 1); + rb_define_singleton_method(klass, "xfree_without_gvl", thread_xfree_without_gvl, 1); } diff --git a/ext/digest/digest.c b/ext/digest/digest.c index 28f60227548400..f0f0910de6421b 100644 --- a/ext/digest/digest.c +++ b/ext/digest/digest.c @@ -13,6 +13,7 @@ ************************************************/ +#define RB_DIGEST_WRAP_METADATA_LINKAGE RUBY_FUNC_EXPORTED #include "digest.h" static VALUE rb_mDigest; @@ -543,7 +544,7 @@ static const rb_data_type_t metadata_type = { {0}, }; -RUBY_FUNC_EXPORTED VALUE +RB_DIGEST_WRAP_METADATA_LINKAGE VALUE rb_digest_wrap_metadata(const rb_digest_metadata_t *meta) { return rb_obj_freeze(TypedData_Wrap_Struct(0, &metadata_type, (void *)meta)); diff --git a/ext/digest/digest.h b/ext/digest/digest.h index c5c37583a6e7c9..0ad8df60b93780 100644 --- a/ext/digest/digest.h +++ b/ext/digest/digest.h @@ -73,13 +73,21 @@ rb_id_metadata(void) # define DIGEST_USE_RB_EXT_RESOLVE_SYMBOL 1 #endif +/* Declarations and definitions of the same function must carry the same + * attribute on MSVC, and digest.c has to export the definition so that + * statically linked extensions can find the symbol in libruby. */ +#ifndef RB_DIGEST_WRAP_METADATA_LINKAGE +# define RB_DIGEST_WRAP_METADATA_LINKAGE extern +#endif + static inline VALUE rb_digest_make_metadata(const rb_digest_metadata_t *meta) { #if defined(EXTSTATIC) && EXTSTATIC /* The extension is built as a static library, so safe to refer to * rb_digest_wrap_metadata directly. */ - extern VALUE rb_digest_wrap_metadata(const rb_digest_metadata_t *meta); + RB_DIGEST_WRAP_METADATA_LINKAGE + VALUE rb_digest_wrap_metadata(const rb_digest_metadata_t *meta); return rb_digest_wrap_metadata(meta); #else /* The extension is built as a shared library, so we can't refer diff --git a/ext/erb/escape/escape.c b/ext/erb/escape/escape.c index 1794fc30ebdce7..19d8964823d573 100644 --- a/ext/erb/escape/escape.c +++ b/ext/erb/escape/escape.c @@ -1,30 +1,19 @@ #include "ruby.h" #include "ruby/encoding.h" -static VALUE rb_cERB, rb_mEscape, rb_cCGI; +static VALUE rb_cCGI; static ID id_escapeHTML; #define HTML_ESCAPE_MAX_LEN 6 -static const struct { - uint8_t len; - char str[HTML_ESCAPE_MAX_LEN+1]; -} html_escape_table[UCHAR_MAX+1] = { -#define HTML_ESCAPE(c, str) [c] = {rb_strlen_lit(str), str} - HTML_ESCAPE('\'', "'"), - HTML_ESCAPE('&', "&"), - HTML_ESCAPE('"', """), - HTML_ESCAPE('<', "<"), - HTML_ESCAPE('>', ">"), -#undef HTML_ESCAPE +static const bool html_escape_table[UCHAR_MAX+1] = { + ['\''] = true, + ['&'] = true, + ['"'] = true, + ['<'] = true, + ['>'] = true, }; -static inline void -preserve_original_state(VALUE orig, VALUE dest) -{ - rb_enc_associate(dest, rb_enc_get(orig)); -} - static inline long escaped_length(VALUE str) { @@ -35,43 +24,263 @@ escaped_length(VALUE str) return len * HTML_ESCAPE_MAX_LEN; } +#ifdef __clang__ +# if __has_builtin(__builtin_ctzll) +# define HAVE_BUILTIN_CTZLL 1 +# else +# define HAVE_BUILTIN_CTZLL 0 +# endif +#elif defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)) +# define HAVE_BUILTIN_CTZLL 1 +#else +# define HAVE_BUILTIN_CTZLL 0 +#endif + +#ifdef ERB_ENABLE_SIMD +#if defined(__amd64__) || defined(__amd64) || defined(__x86_64__) || defined(__x86_64) || defined(_M_X64) || defined(_M_AMD64) +#ifdef HAVE_X86INTRIN_H +#include +#define HAVE_SIMD 1 +#define HAVE_SIMD_SSE2 1 +#endif +#endif + +#if defined(__ARM_NEON) || defined(__ARM_NEON__) || defined(__aarch64__) || defined(_M_ARM64) +#define HAVE_SIMD 1 +#define HAVE_SIMD_NEON 1 +#include +#endif +#endif // ERB_ENABLE_SIMD + +typedef struct _search_state { + const unsigned char *cstr; + const unsigned char *end; + +#if defined(HAVE_SIMD_NEON) + uint64_t matches_bitmap; +#elif defined(HAVE_SIMD_SSE2) + int matches_bitmap; +#endif +} search_state; + +static inline bool +find_next_basic(search_state *search) +{ + while (search->cstr < search->end) { + if (html_escape_table[*search->cstr]) { + return true; + } + search->cstr++; + } + return false; +} + +#ifdef HAVE_SIMD_SSE2 + +static inline int trailing_zeros(int input) +{ + RUBY_ASSERT(input > 0); // __builtin_ctz(0) is undefined behavior + +#if HAVE_BUILTIN_CTZLL + return __builtin_ctz(input); +#else + int trailing_zeros = 0; + int temp = input; + while ((temp & 1) == 0 && temp > 0) { + trailing_zeros++; + temp >>= 1; + } + return trailing_zeros; +#endif +} + +static inline bool +find_next_match_sse2(search_state *search) +{ + int next_match_offset = trailing_zeros(search->matches_bitmap); + search->matches_bitmap >>= (next_match_offset + 1); + search->cstr += next_match_offset; + if (search->cstr > search->end) { + search->cstr = search->end; + return false; + } + return true; +} + +static inline bool +find_next_sse2(search_state *search) +{ + if (search->matches_bitmap) { + return find_next_match_sse2(search); + } + + const __m128i single_quote = _mm_set1_epi8('\''); + const __m128i double_quote = _mm_set1_epi8('"'); + const __m128i ampersand = _mm_set1_epi8('&'); + const __m128i lt = _mm_set1_epi8('<'); + const __m128i gt = _mm_set1_epi8('>'); + + while ((size_t)(search->end - search->cstr) >= sizeof(__m128i)) { + const __m128i bytes = _mm_loadu_si128((__m128i const *)search->cstr); + const __m128i match1 = _mm_cmpeq_epi8(bytes, single_quote); + const __m128i match2 = _mm_cmpeq_epi8(bytes, double_quote); + const __m128i match3 = _mm_cmpeq_epi8(bytes, ampersand); + const __m128i match4 = _mm_cmpeq_epi8(bytes, lt); + const __m128i match5 = _mm_cmpeq_epi8(bytes, gt); + + const __m128i mask1 = _mm_or_si128(match1, match2); + const __m128i mask2 = _mm_or_si128(match3, match4); + const __m128i mask3 = _mm_or_si128(mask1, match5); + const __m128i matches = _mm_or_si128(mask2, mask3); + + const int bitmap = _mm_movemask_epi8(matches); + + if (bitmap) { + search->matches_bitmap = bitmap; + return find_next_match_sse2(search); + } + search->cstr += sizeof(__m128i); + } + + return find_next_basic(search); +} +#define find_next find_next_sse2 +#endif + +#ifdef HAVE_SIMD_NEON +#ifndef __has_builtin // Optional of course. + #define __has_builtin(x) 0 // Compatibility with non-clang compilers. +#endif + +static inline uint32_t trailing_zeros64(uint64_t input) +{ +#if HAVE_BUILTIN_CTZLL + return __builtin_ctzll(input); +#else + uint32_t trailing_zeros = 0; + uint64_t temp = input; + while ((temp & 1) == 0 && temp > 0) { + trailing_zeros++; + temp >>= 1; + } + return trailing_zeros; +#endif +} + +static inline bool +find_next_match_neon(search_state *search) +{ + size_t next_match_offset = trailing_zeros64(search->matches_bitmap) / 4; + search->matches_bitmap >>= (next_match_offset + 1) * 4; + search->cstr += next_match_offset; + if (search->cstr > search->end) { + search->cstr = search->end; + return false; + } + return true; +} + +static inline bool +find_next_neon(search_state *search) +{ + if (search->matches_bitmap) { + return find_next_match_neon(search); + } + + const uint8x16_t single_quote = vdupq_n_u8('\''); + const uint8x16_t double_quote = vdupq_n_u8('"'); + const uint8x16_t ampersand = vdupq_n_u8('&'); + const uint8x16_t lt = vdupq_n_u8('<'); + const uint8x16_t gt = vdupq_n_u8('>'); + + while ((size_t)(search->end - search->cstr) >= sizeof(uint8x16_t)) { + const uint8x16_t bytes = vld1q_u8(search->cstr); + const uint8x16_t match1 = vceqq_u8(bytes, single_quote); + const uint8x16_t match2 = vceqq_u8(bytes, double_quote); + const uint8x16_t match3 = vceqq_u8(bytes, ampersand); + const uint8x16_t match4 = vceqq_u8(bytes, lt); + const uint8x16_t match5 = vceqq_u8(bytes, gt); + + const uint8x16_t mask1 = vorrq_u8(match1, match2); + const uint8x16_t mask2 = vorrq_u8(match3, match4); + const uint8x16_t mask3 = vorrq_u8(mask1, match5); + const uint8x16_t matches = vorrq_u8(mask2, mask3); + + const uint8x8_t res = vshrn_n_u16(vreinterpretq_u16_u8(matches), 4); + const uint64_t bitmap = vget_lane_u64(vreinterpret_u64_u8(res), 0) & 0x8888888888888888ull; + + if (bitmap) { + search->matches_bitmap = bitmap; + return find_next_match_neon(search); + } + search->cstr += sizeof(uint8x16_t); + } + + return find_next_basic(search); +} + +#define find_next find_next_neon +#endif // HAVE_SIMD_NEON + +#ifndef find_next +#define find_next_basic +#endif + static VALUE optimized_escape_html(VALUE str) { VALUE vbuf; char *buf = NULL; - const char *cstr = RSTRING_PTR(str); - const char *end = cstr + RSTRING_LEN(str); + search_state search = { + .cstr = (const unsigned char *)RSTRING_PTR(str), + }; + search.end = search.cstr + RSTRING_LEN(str); - const char *segment_start = cstr; + const unsigned char *segment_start = search.cstr; char *dest = NULL; - while (cstr < end) { - const unsigned char c = *cstr++; - uint8_t len = html_escape_table[c].len; - if (len) { - size_t segment_len = cstr - segment_start - 1; - if (!buf) { - buf = ALLOCV_N(char, vbuf, escaped_length(str)); - dest = buf; - } - if (segment_len) { - memcpy(dest, segment_start, segment_len); - dest += segment_len; - } - segment_start = cstr; - memcpy(dest, html_escape_table[c].str, len); - dest += len; + + while (find_next(&search)) { + const unsigned char c = *search.cstr; + size_t segment_len = search.cstr - segment_start; + search.cstr++; + + if (!buf) { + buf = ALLOCV_N(char, vbuf, escaped_length(str)); + dest = buf; + } + if (segment_len) { + memcpy(dest, segment_start, segment_len); + dest += segment_len; + } + segment_start = search.cstr; + + switch(c) { + #define HTML_ESCAPE(c, str) \ + case c: \ + memcpy(dest, str, rb_strlen_lit(str)); \ + dest += rb_strlen_lit(str); \ + break + + HTML_ESCAPE('\'', "'"); + HTML_ESCAPE('&', "&"); + HTML_ESCAPE('"', """); + HTML_ESCAPE('<', "<"); + HTML_ESCAPE('>', ">"); + default: + UNREACHABLE_RETURN(Qundef); + + #undef HTML_ESCAPE } } + VALUE escaped = str; if (buf) { - size_t segment_len = cstr - segment_start; + size_t segment_len = search.cstr - segment_start; if (segment_len) { memcpy(dest, segment_start, segment_len); dest += segment_len; } - escaped = rb_str_new(buf, dest - buf); - preserve_original_state(str, escaped); + escaped = rb_enc_str_new(buf, dest - buf, rb_enc_get(str)); ALLOCV_END(vbuf); } return escaped; @@ -105,8 +314,8 @@ Init_escape(void) rb_ext_ractor_safe(true); #endif - rb_cERB = rb_define_class("ERB", rb_cObject); - rb_mEscape = rb_define_module_under(rb_cERB, "Escape"); + VALUE rb_cERB = rb_define_class("ERB", rb_cObject); + VALUE rb_mEscape = rb_define_module_under(rb_cERB, "Escape"); rb_define_module_function(rb_mEscape, "html_escape", erb_escape_html, 1); rb_cCGI = rb_define_class("CGI", rb_cObject); diff --git a/ext/erb/escape/extconf.rb b/ext/erb/escape/extconf.rb index b211a9783f459e..f00147b8b9ce99 100644 --- a/ext/erb/escape/extconf.rb +++ b/ext/erb/escape/extconf.rb @@ -5,5 +5,27 @@ File.write('Makefile', dummy_makefile($srcdir).join) else have_func("rb_ext_ractor_safe", "ruby.h") + + case RbConfig::CONFIG['host_cpu'] + when /^(arm|aarch64)/ + # Try to compile a small program using NEON instructions + header, type, init, extra = 'arm_neon.h', 'uint8x16_t', 'vdupq_n_u8(32)', nil + when /^(x86_64|x64)/ + header, type, init, extra = 'x86intrin.h', '__m128i', '_mm_set1_epi8(32)', 'if (__builtin_cpu_supports("sse2")) { printf("OK"); }' + end + if header + if have_header(header) && try_compile(<<~SRC, '-Werror=implicit-function-declaration') + #{cpp_include(header)} + int main(int argc, char **argv) { + #{type} test = #{init}; + #{extra} + if (argc > 100000) printf("%p", &test); + return 0; + } + SRC + $defs.push("-DERB_ENABLE_SIMD") + end + end + create_makefile 'erb/escape' end diff --git a/ext/extmk.rb b/ext/extmk.rb index 44f86e19734deb..d4127fbfdd4587 100755 --- a/ext/extmk.rb +++ b/ext/extmk.rb @@ -781,6 +781,7 @@ def mf.macro(name, values, max = 70) end submakeopts << 'EXTLDFLAGS="$(EXTLDFLAGS)"' submakeopts << 'EXTINITS="$(EXTINITS)"' + submakeopts << 'PROGRAM_EXTS=' submakeopts << 'SHOWFLAGS=' mf.macro "SUBMAKEOPTS", submakeopts mf.macro "NOTE_MESG", %w[$(RUBY) $(top_srcdir)/tool/lib/colorize.rb skip] diff --git a/ext/json/lib/json.rb b/ext/json/lib/json.rb index 1e7573df97031b..f5f5ade89e1666 100644 --- a/ext/json/lib/json.rb +++ b/ext/json/lib/json.rb @@ -44,13 +44,13 @@ # # You can parse a \String containing \JSON data using # either of two methods: -# - JSON.parse(source, opts) -# - JSON.parse!(source, opts) +# - JSON.parse(source, **opts) +# - JSON.parse!(source, **opts) # # where # - +source+ is a Ruby object. -# - +opts+ is a \Hash object containing options -# that control both input allowed and output formatting. +# - +opts+ are keyword arguments that control both input +# allowed and output formatting. # # The difference between the two methods # is that JSON.parse! omits some checks @@ -102,7 +102,7 @@ # ruby # => 1.0 # ruby.class # => Float # ruby = JSON.parse('2.0e2') -# ruby # => 200 +# ruby # => 200.0 # ruby.class # => Float # Boolean: # ruby = JSON.parse('true') @@ -131,10 +131,10 @@ # ruby # => [0, [1, [2, [3]]]] # Too deep: # # Raises JSON::NestingError (nesting of 2 is too deep): -# JSON.parse(source, {max_nesting: 1}) +# JSON.parse(source, max_nesting: 1) # Bad value: -# # Raises TypeError (wrong argument type Symbol (expected Fixnum)): -# JSON.parse(source, {max_nesting: :foo}) +# # Raises TypeError (no implicit conversion of Symbol into Integer): +# JSON.parse(source, max_nesting: :foo) # # --- # @@ -142,11 +142,11 @@ # should be ignored or cause an error to be raised: # # When set to +false+, the default: -# JSON.parse('{"a": 1, "a":2}') => duplicate key at line 1 column 1 (JSON::ParserError) +# JSON.parse('{"a": 1, "a": 2}') # duplicate key "a" at line 1 column 1 (JSON::ParserError) # # When set to +true+: # # The last value is used. -# JSON.parse('{"a": 1, "a":2}', allow_duplicate_key: true) => {"a" => 2} +# JSON.parse('{"a": 1, "a": 2}', allow_duplicate_key: true) # => {"a" => 2} # # --- # @@ -155,15 +155,15 @@ # defaults to +false+. # # With the default, +false+: -# # Raises JSON::ParserError (225: unexpected token at '[NaN]'): +# # Raises JSON::ParserError (unexpected token 'NaN]' at line 1 column 2): # JSON.parse('[NaN]') -# # Raises JSON::ParserError (232: unexpected token at '[Infinity]'): +# # Raises JSON::ParserError (unexpected token 'Infinity]' at line 1 column 2): # JSON.parse('[Infinity]') -# # Raises JSON::ParserError (248: unexpected token at '[-Infinity]'): +# # Raises JSON::ParserError (invalid number: '-Infinity]' at line 1 column 2): # JSON.parse('[-Infinity]') # Allow: # source = '[NaN, Infinity, -Infinity]' -# ruby = JSON.parse(source, {allow_nan: true}) +# ruby = JSON.parse(source, allow_nan: true) # ruby # => [NaN, Infinity, -Infinity] # # --- @@ -185,10 +185,10 @@ # defaults to +false+. # # When set to +false+, the default: -# JSON.parse('/* comment */ {"a": 1, "a":2}') # unexpected character: '/' at line 1 column 1 (JSON::ParserError) +# JSON.parse('/* comment */ {"a": 1, "a": 2}') # unexpected token '/*' at line 1 column 1 (JSON::ParserError) # # When set to +true+, comments are ignored: -# JSON.parse('/* comment */ {"a": 1, "a":2} // more comment') # => {"a" => 2} +# JSON.parse('/* comment */ {"a": 1} // more comment', allow_comments: true) # => {"a" => 1} # # --- # @@ -197,7 +197,7 @@ # defaults to +false+. # # With the default, +false+: -# JSON.parse(%{"Hello\nWorld"}) # invalid ASCII control character in string (JSON::ParserError) +# JSON.parse(%{"Hello\nWorld"}) # invalid ASCII control character in string: \nWorld" at line 2 column 0 (JSON::ParserError) # # When enabled: # JSON.parse(%{"Hello\nWorld"}, allow_control_characters: true) # => "Hello\nWorld" @@ -209,7 +209,7 @@ # defaults to +false+. # # With the default, +false+: -# JSON.parse('"Hell\o"') # invalid escape character in string (JSON::ParserError) +# JSON.parse('"Hell\o"') # invalid escape character in string: '\o"' at line 1 column 6 (JSON::ParserError) # # When enabled: # JSON.parse('"Hell\o"', allow_invalid_escape: true) # => "Hello" @@ -228,8 +228,8 @@ # ruby = JSON.parse(source) # ruby # => {"a"=>"foo", "b"=>1.0, "c"=>true, "d"=>false, "e"=>nil} # Use Symbols: -# ruby = JSON.parse(source, {symbolize_names: true}) -# ruby # => {:a=>"foo", :b=>1.0, :c=>true, :d=>false, :e=>nil} +# ruby = JSON.parse(source, symbolize_names: true) +# ruby # => {a: "foo", b: 1.0, c: true, d: false, e: nil} # # --- # @@ -242,7 +242,7 @@ # ruby = JSON.parse(source) # ruby.class # => Hash # Use class \OpenStruct: -# ruby = JSON.parse(source, {object_class: OpenStruct}) +# ruby = JSON.parse(source, object_class: OpenStruct) # ruby # => # # # --- @@ -256,8 +256,8 @@ # ruby = JSON.parse(source) # ruby.class # => Array # Use class \Set: -# ruby = JSON.parse(source, {array_class: Set}) -# ruby # => # +# ruby = JSON.parse(source, array_class: Set) +# ruby # => Set["foo", 1.0, true, false, nil] # # === Generating \JSON # @@ -319,22 +319,22 @@ # a \String containing a \JSON string representation of the source: # JSON.generate(:foo) # => '"foo"' # JSON.generate(Complex(0, 0)) # => '"0+0i"' -# JSON.generate(Dir.new('.')) # => '"#"' +# JSON.generate(Dir.new('.')) # => '"#"' # # ==== Generating Options # # ====== Input Options # # Option +allow_nan+ (boolean) specifies whether -# +NaN+, +Infinity+, and -Infinity may be generated; +# +NaN+, +Infinity+, and +-Infinity+ may be generated; # defaults to +false+. # # With the default, +false+: -# # Raises JSON::GeneratorError (920: NaN not allowed in JSON): +# # Raises JSON::GeneratorError (NaN not allowed in JSON): # JSON.generate(JSON::NaN) -# # Raises JSON::GeneratorError (917: Infinity not allowed in JSON): +# # Raises JSON::GeneratorError (Infinity not allowed in JSON): # JSON.generate(JSON::Infinity) -# # Raises JSON::GeneratorError (917: -Infinity not allowed in JSON): +# # Raises JSON::GeneratorError (-Infinity not allowed in JSON): # JSON.generate(JSON::MinusInfinity) # # Allow: @@ -345,14 +345,14 @@ # # Option +allow_duplicate_key+ (boolean) specifies whether # hashes with duplicate keys should be allowed or produce an error. -# defaults to emit a deprecation warning. +# Defaults to +false+, which raises an error. # -# With the default, false: -# JSON.generate({ foo: 1, "foo" => 2 }) +# With the default, +false+: +# JSON.generate({foo: 1, "foo" => 2}) # # detected duplicate key "foo" in {foo: 1, "foo" => 2} (JSON::GeneratorError) # -# With true -# JSON.generate({ foo: 1, "foo" => 2 }, allow_duplicate_key: true) +# With +true+: +# JSON.generate({foo: 1, "foo" => 2}, allow_duplicate_key: true) # # => '{"foo":1,"foo":2}' # # --- @@ -365,13 +365,13 @@ # JSON.generate(obj) # => '[[[[[[0]]]]]]' # # Too deep: -# # Raises JSON::NestingError (nesting of 2 is too deep): +# # Raises JSON::NestingError (nesting of 2 is too deep. Did you try to serialize objects with circular references?): # JSON.generate(obj, max_nesting: 2) # # With +false+: # obj = [] # obj[0] = obj -# # Raises SystemStackError: stack level too deep +# # Raises SystemStackError (stack level too deep): # JSON.generate(obj, max_nesting: false) # # Setting +max_nesting+ to +false+ or a very large number can lead to a stack overflow @@ -410,7 +410,7 @@ # inserted before the colon in each \JSON object's pair; # defaults to the empty \String, ''. # - Option +sort_keys+ (boolean or \Proc) controls whether and how the keys of a -# hash are sorted when generating the output; defaults to false. +# hash are sorted when generating the output; defaults to +false+. # When +true+, keys are sorted lexicographically. When a \Proc, it receives # the entire \Hash and must return a \Hash with its pairs in the desired # order, allowing for arbitrary sort orders. @@ -439,7 +439,7 @@ # "foo" : [ # "bar", # "baz" -# ], +# ], # "bat" : { # "bam" : 0, # "bad" : 1 diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb index edcd93d4d442d2..82d334061e29d5 100644 --- a/ext/json/lib/json/common.rb +++ b/ext/json/lib/json/common.rb @@ -143,11 +143,11 @@ class JSONError < StandardError; end # This exception is raised if a parser error occurs. class ParserError < JSONError # Line number where the parser encountered an error. - # Is nil when raised by JSON::ResumableParser. + # Is +nil+ when raised by JSON::ResumableParser. attr_reader :line # Column number where the parser encountered an error. - # Is nil when raised by JSON::ResumableParser. + # Is +nil+ when raised by JSON::ResumableParser. attr_reader :column # Returns a best effort JSONPath string representing where in the document @@ -290,7 +290,7 @@ def to_json(state = nil, *) # --- # # Raises an exception if +source+ is not valid JSON: - # # Raises JSON::ParserError unexpected character: 'invalid' at line 1 column 1 : + # # Raises JSON::ParserError (unexpected character: 'invalid' at line 1 column 1): # JSON.parse('invalid') # def parse(source, on_load: nil, object_class: nil, array_class: nil, **options) @@ -372,7 +372,7 @@ def load_file!(filespec, **options) # # Raises an exception if +obj+ contains circular references: # a = []; b = []; a.push(b); b.push(a) - # # Raises JSON::NestingError (nesting of 100 is too deep): + # # Raises JSON::NestingError (nesting of 100 is too deep. Did you try to serialize objects with circular references?): # JSON.generate(a) # def generate(obj, opts = nil) @@ -812,9 +812,9 @@ class Coder private_constant :EXCLUDED_GENERATOR_OPTIONS # :call-seq: - # JSON.new(options = nil, &block) + # JSON::Coder.new(**options, &block) # - # Argument +options+, if given, contains a \Hash of options for both parsing and generating. + # Keyword arguments +options+, if given, are options for both parsing and generating. # See {Parsing Options}[rdoc-ref:JSON@Parsing+Options], # and {Generating Options}[rdoc-ref:JSON@Generating+Options]. # diff --git a/ext/json/lib/json/version.rb b/ext/json/lib/json/version.rb index 1cf83a03954159..318272defdad67 100644 --- a/ext/json/lib/json/version.rb +++ b/ext/json/lib/json/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module JSON - VERSION = '3.0.1' + VERSION = '3.0.2' end diff --git a/file.c b/file.c index 7f0967e5c18719..42ffc6147319bc 100644 --- a/file.c +++ b/file.c @@ -1883,18 +1883,16 @@ rb_file_pipe_p(VALUE obj, VALUE fname) * call-seq: * File.symlink?(path) -> true or false * - * Returns whether the entry at `path` is a symbolic link: + * Returns whether the entry at `path` + * is a [symbolic link](rdoc-ref:file/symbolic_links.md): * * ```ruby - * # Create paths. - * file_path = 'doc/extension.rdoc' # => "doc/extension.rdoc" - * target_path = File.join('..', file_path) # => "../doc/extension.rdoc" - * link_path = 'lib/u.tmp' # => "lib/u.tmp" - * File.symlink?(link_path) # => false - * # Create link and verify. - * File.symlink(target_path, link_path) - * File.symlink?(link_path) # => true - * File.delete(link_path) # Clean up. + * filepath = 'README.md' + * linkpath = 'foo' + * File.symlink(filepath, linkpath) + * File.symlink?(filepath) # => false + * File.symlink?(linkpath) # => true + * File.unlink(linkpath) # Clean up. * ``` * */ @@ -6721,18 +6719,17 @@ rb_stat_p(VALUE obj) * call-seq: * symlink? -> true or false * - * Returns whether the entry in `self` is a symbolic link: + * Returns whether the entry in `self` + * is a [symbolic link](rdoc-ref:file/symbolic_links.md): * * ```ruby - * path = 'doc/t.tmp' - * link_path = 'lib/u.tmp' - * File.write(path, 'foo') - * File.symlink(path, link_path) - * File.stat(path).symlink? # => false - * File.stat(link_path).symlink? # Raises Errno::ENOENT; entry is not a file. - * File.lstat(link_path).symlink? # => true - * File.delete(path) - * File.delete(link_path) + * filepath = 'README.md' + * linkpath = 'foo' + * File.symlink(filepath, linkpath) + * File.stat(filepath).symlink? # => false + * File.stat(linkpath).symlink? # => false # stat followed symlink. + * File.lstat(linkpath).symlink? # => true # lstat did not follow symlink. + * File.unlink(linkpath) # Clean up. * ``` * */ diff --git a/gc.c b/gc.c index 793e02b0503d7a..71c5c163f0b0de 100644 --- a/gc.c +++ b/gc.c @@ -1129,20 +1129,17 @@ gc_newobj_hook(VALUE obj) RB_GC_VM_UNLOCK_NO_BARRIER(lev); } -ALWAYS_INLINE(static VALUE newobj_body(rb_execution_context_t *ec, VALUE klass, VALUE flags, shape_id_t shape_id, bool wb_protected, size_t size)); +ALWAYS_INLINE(static VALUE newobj_body(rb_ractor_t *cr, void *objspace, VALUE klass, VALUE flags, shape_id_t shape_id, bool wb_protected, size_t size)); /* The allocation body shared by rb_newobj and rb_ec_newobj_of, forced inline into * both: left to this big translation unit's inline budget, gcc drops it from one * entry point or the other and that allocation path grows a call. */ static VALUE -newobj_body(rb_execution_context_t *ec, VALUE klass, VALUE flags, shape_id_t shape_id, bool wb_protected, size_t size) +newobj_body(rb_ractor_t *cr, void *objspace, VALUE klass, VALUE flags, shape_id_t shape_id, bool wb_protected, size_t size) { GC_ASSERT((flags & FL_WB_PROTECTED) == 0); - rb_ractor_t *cr = rb_ec_ractor_ptr(ec); - /* Use cr->objspace directly: rb_gc_get_objspace() would look cr up through TLS - * on every allocation. */ size_t actual_alloc_size; - VALUE obj = rb_gc_impl_new_obj(cr->objspace, cr->newobj_cache, klass, flags, wb_protected, size, &actual_alloc_size); + VALUE obj = rb_gc_impl_new_obj(objspace, cr->newobj_cache, klass, flags, wb_protected, size, &actual_alloc_size); GC_ASSERT(actual_alloc_size >= size); shape_id = rb_shape_transition_slot_size(shape_id, actual_alloc_size); @@ -1177,7 +1174,30 @@ newobj_body(rb_execution_context_t *ec, VALUE klass, VALUE flags, shape_id_t sha VALUE rb_newobj(rb_execution_context_t *ec, VALUE klass, VALUE flags, shape_id_t shape_id, bool wb_protected, size_t size) { - return newobj_body(ec, klass, flags, shape_id, wb_protected, size); + /* Read the Ractor's slot directly: rb_gc_get_objspace() would look cr up through + * TLS on every allocation. */ + rb_ractor_t *cr = rb_ec_ractor_ptr(ec); + return newobj_body(cr, cr->objspace, klass, flags, shape_id, wb_protected, size); +} + +/* Build the object in a named objspace rather than the current Ractor's. The only + * foreign objspace allowed is the child this Ractor is building + * (create_ractor_alloc_thread), whose wrappers must be objects the child owns. + * + * That target has no thread of its own yet, which is what makes this cheap: nothing + * allocates, sweeps or collects there, so the half-built objects need no root and its + * GC can be suppressed outright. Aiming at a live Ractor's objspace -- to build a + * copy where it will be used, say -- needs three things this does not have: a root the + * target's own GC marks the objects under construction from, a newobj_cache paired + * with that objspace (today the only multi-objspace collector has no per-Ractor cache, + * and the ones that do are single-objspace), and write barriers aimed at the target. + * The assertion stops the shortcut. */ +static VALUE +rb_newobj_in_objspace(rb_execution_context_t *ec, void *objspace, VALUE klass, VALUE flags, shape_id_t shape_id, bool wb_protected, size_t size) +{ + rb_ractor_t *cr = rb_ec_ractor_ptr(ec); + RUBY_ASSERT(objspace == cr->objspace || objspace == cr->creating_child_objspace); + return newobj_body(cr, objspace, klass, flags, shape_id, wb_protected, size); } VALUE @@ -1191,7 +1211,8 @@ rb_ec_newobj_of(rb_execution_context_t *ec, VALUE klass, VALUE flags, size_t siz RUBY_ASSERT(type != T_ICLASS); (void)type; - return newobj_body(ec, klass, flags, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, true, size); + rb_ractor_t *cr = rb_ec_ractor_ptr(ec); + return newobj_body(cr, cr->objspace, klass, flags, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, true, size); } static VALUE @@ -1318,12 +1339,12 @@ rb_data_object_check(VALUE klass) #define RTYPEDDATA_EMBEDDABLE_P(obj) RB_DATA_TYPE_EMBEDDABLE_P(RTYPEDDATA_TYPE(obj)) static VALUE -typed_data_alloc(VALUE klass, VALUE typed_flag, void *datap, const rb_data_type_t *type, size_t size) +typed_data_alloc_in(void *objspace, VALUE klass, VALUE typed_flag, void *datap, const rb_data_type_t *type, size_t size) { RBIMPL_NONNULL_ARG(type); if (klass) rb_data_object_check(klass); bool wb_protected = (type->flags & RUBY_FL_WB_PROTECTED) || !type->function.dmark; - VALUE obj = rb_newobj(GET_EC(), klass, T_DATA, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_RDATA, wb_protected, size); + VALUE obj = rb_newobj_in_objspace(GET_EC(), objspace, klass, T_DATA, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_RDATA, wb_protected, size); rb_gc_register_pinning_obj(obj); @@ -1335,18 +1356,30 @@ typed_data_alloc(VALUE klass, VALUE typed_flag, void *datap, const rb_data_type_ return obj; } -VALUE -rb_data_typed_object_wrap(VALUE klass, void *datap, const rb_data_type_t *type) +static VALUE +typed_data_wrap_in(void *objspace, VALUE klass, void *datap, const rb_data_type_t *type) { if (UNLIKELY(RB_DATA_TYPE_EMBEDDABLE_P(type))) { rb_raise(rb_eTypeError, "Cannot wrap an embeddable TypedData"); } - return typed_data_alloc(klass, 0, datap, type, sizeof(struct RTypedData)); + return typed_data_alloc_in(objspace, klass, 0, datap, type, sizeof(struct RTypedData)); } VALUE -rb_data_typed_object_zalloc(VALUE klass, size_t size, const rb_data_type_t *type) +rb_data_typed_object_wrap(VALUE klass, void *datap, const rb_data_type_t *type) +{ + return typed_data_wrap_in(rb_ec_ractor_ptr(GET_EC())->objspace, klass, datap, type); +} + +VALUE +rb_data_typed_object_wrap_in_objspace(void *objspace, VALUE klass, void *datap, const rb_data_type_t *type) +{ + return typed_data_wrap_in(objspace, klass, datap, type); +} + +static VALUE +typed_data_zalloc_in(void *objspace, VALUE klass, size_t size, const rb_data_type_t *type) { if (RB_DATA_TYPE_EMBEDDABLE_P(type)) { if (!(type->flags & (RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_THREAD_SAFE_FREE))) { @@ -1355,17 +1388,29 @@ rb_data_typed_object_zalloc(VALUE klass, size_t size, const rb_data_type_t *type size_t embed_size = offsetof(struct RTypedData, data) + size; if (rb_gc_size_allocatable_p(embed_size)) { - VALUE obj = typed_data_alloc(klass, TYPED_DATA_EMBEDDED, 0, type, embed_size); + VALUE obj = typed_data_alloc_in(objspace, klass, TYPED_DATA_EMBEDDED, 0, type, embed_size); memset((char *)obj + offsetof(struct RTypedData, data), 0, size); return obj; } } - VALUE obj = typed_data_alloc(klass, 0, NULL, type, sizeof(struct RTypedData)); + VALUE obj = typed_data_alloc_in(objspace, klass, 0, NULL, type, sizeof(struct RTypedData)); DATA_PTR(obj) = xcalloc(1, size); return obj; } +VALUE +rb_data_typed_object_zalloc(VALUE klass, size_t size, const rb_data_type_t *type) +{ + return typed_data_zalloc_in(rb_ec_ractor_ptr(GET_EC())->objspace, klass, size, type); +} + +VALUE +rb_data_typed_object_zalloc_in_objspace(void *objspace, VALUE klass, size_t size, const rb_data_type_t *type) +{ + return typed_data_zalloc_in(objspace, klass, size, type); +} + static size_t ruby_xmalloc_usable_size(void *ptr) { @@ -2052,17 +2097,14 @@ os_obj_of(VALUE of) * Because every live object is visited, this method is mainly useful for * debugging, profiling, and introspecting a running process. * - * Due to a current Ractor implementation issue, this method does not yield - * Ractor-unshareable objects when the process is in multi-Ractor mode. - * Multi-Ractor mode is enabled when Ractor.new has been called for the first - * time. See https://bugs.ruby-lang.org/issues/19387 for more information. + * In multi-Ractor mode this method yields every object of the current Ractor, plus + * the objects of the other Ractors that have been made Ractor-shareable. Another + * Ractor's unshareable objects are never yielded: they belong to that Ractor and the + * current one must not touch them. * - * a = 12345678987654321 # shareable - * b = [].freeze # shareable - * c = {} # not shareable - * ObjectSpace.each_object {|x| x } # yields a, b, and c - * Ractor.new {} # enter multi-Ractor mode - * ObjectSpace.each_object {|x| x } # does not yield c + * c = {} # not shareable, belongs to the main Ractor + * r = Ractor.new { d = {}; receive } # d belongs to r + * ObjectSpace.each_object {|x| x } # yields c, but not d * */ @@ -3107,6 +3149,15 @@ rb_gc_location(VALUE value) return gc_location_internal(rb_gc_get_objspace(), value); } +void +rb_gc_update_moved(VALUE *ptr) +{ + VALUE destination = rb_gc_location(*ptr); + if (destination != *ptr) { + *ptr = destination; + } +} + #if defined(__wasm__) @@ -4421,9 +4472,7 @@ rb_gc_update_set_refs_i(st_data_t key, st_data_t value, st_data_t argp, int erro static int rb_gc_update_set_refs_replace_i(st_data_t *key, st_data_t *value, st_data_t argp, int existing) { - if (rb_gc_location((VALUE)*key) != (VALUE)*key) { - *key = rb_gc_location((VALUE)*key); - } + rb_gc_update_moved((VALUE *)key); return ST_CONTINUE; } @@ -5366,22 +5415,33 @@ rb_objspace_gc_disable(void *objspace) return RBOOL(disabled); } +VALUE +rb_gc_objspace_enable(void *objspace) +{ + return rb_objspace_gc_enable(objspace); +} + VALUE rb_gc_local_enable(void) { - return rb_objspace_gc_enable(rb_gc_get_objspace()); + return rb_gc_objspace_enable(rb_gc_get_objspace()); } VALUE -rb_gc_local_disable_no_rest(void) +rb_gc_objspace_disable_no_rest(void *objspace) { - void *objspace = rb_gc_get_objspace(); bool disabled = !rb_gc_impl_gc_enabled_p(objspace); rb_gc_impl_gc_disable(objspace, false); return RBOOL(disabled); } +VALUE +rb_gc_local_disable_no_rest(void) +{ + return rb_gc_objspace_disable_no_rest(rb_gc_get_objspace()); +} + static VALUE gc_enable(rb_execution_context_t *ec, VALUE _) { diff --git a/gc/default/default.c b/gc/default/default.c index a6afd9e65c97e9..41417f13f377b2 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -9936,7 +9936,7 @@ gc_update_references_weak_table_i(VALUE obj, void *data) static int gc_update_references_weak_table_replace_i(VALUE *obj, void *data) { - *obj = rb_gc_location(*obj); + rb_gc_update_moved(obj); return ST_CONTINUE; } diff --git a/gc/gc.h b/gc/gc.h index 80a9ffd92f6119..d77ab4ab1296ce 100644 --- a/gc/gc.h +++ b/gc/gc.h @@ -171,7 +171,7 @@ hash_foreach_replace_value(st_data_t key, st_data_t value, st_data_t argp, int e static int hash_replace_ref_value(st_data_t *key, st_data_t *value, st_data_t argp, int existing) { - *value = rb_gc_location((VALUE)*value); + rb_gc_update_moved((VALUE *)value); return ST_CONTINUE; } @@ -219,15 +219,8 @@ hash_foreach_replace(st_data_t key, st_data_t value, st_data_t argp, int error) static int hash_replace_ref(st_data_t *key, st_data_t *value, st_data_t argp, int existing) { - VALUE new_key = rb_gc_location((VALUE)*key); - if (new_key != (VALUE)*key) { - *key = new_key; - } - - VALUE new_value = rb_gc_location((VALUE)*value); - if (new_value != (VALUE)*value) { - *value = new_value; - } + rb_gc_update_moved((VALUE *)key); + rb_gc_update_moved((VALUE *)value); return ST_CONTINUE; } diff --git a/id_table.c b/id_table.c index 7a780bb9e0bcf5..1b0dc756d790ac 100644 --- a/id_table.c +++ b/id_table.c @@ -515,7 +515,7 @@ marked_id_table_compact_check_i(VALUE value, void *data) static enum rb_id_table_iterator_result marked_id_table_compact_replace_i(VALUE *value, void *data, int existing) { - *value = rb_gc_location(*value); + rb_gc_update_moved(value); return ID_TABLE_CONTINUE; } diff --git a/imemo.c b/imemo.c index 8ef12feb7e8de8..bd804fcad64f27 100644 --- a/imemo.c +++ b/imemo.c @@ -422,9 +422,8 @@ rb_imemo_mark_and_move(VALUE obj, bool reference_updating) */ } else if (reference_updating) { - *((VALUE *)&cc->klass) = rb_gc_location(cc->klass); - *((struct rb_callable_method_entry_struct **)&cc->cme_) = - (struct rb_callable_method_entry_struct *)rb_gc_location((VALUE)cc->cme_); + rb_gc_update_moved((VALUE *)&cc->klass); + rb_gc_update_moved_ptr((struct rb_callable_method_entry_struct **)&cc->cme_); RUBY_ASSERT(RB_TYPE_P(cc->klass, T_CLASS) || RB_TYPE_P(cc->klass, T_ICLASS)); RUBY_ASSERT(IMEMO_TYPE_P((VALUE)cc->cme_, imemo_ment)); @@ -488,7 +487,7 @@ rb_imemo_mark_and_move(VALUE obj, bool reference_updating) } if (reference_updating) { - ((VALUE *)env->ep)[VM_ENV_DATA_INDEX_ENV] = rb_gc_location(env->ep[VM_ENV_DATA_INDEX_ENV]); + rb_gc_update_moved(&((VALUE *)env->ep)[VM_ENV_DATA_INDEX_ENV]); } else { if (!VM_ENV_FLAGS(env->ep, VM_ENV_FLAG_WB_REQUIRED)) { @@ -564,7 +563,7 @@ rb_imemo_mark_and_move(VALUE obj, bool reference_updating) VALUE *entries = rb_imemo_subclasses_entries(obj); for (uint32_t i = 0; i < subs->count; i++) { if (entries[i]) { - entries[i] = rb_gc_location(entries[i]); + rb_gc_update_moved(&entries[i]); } } } diff --git a/include/ruby/internal/core/rtypeddata.h b/include/ruby/internal/core/rtypeddata.h index 6b083321cbb47e..90759cfe66402b 100644 --- a/include/ruby/internal/core/rtypeddata.h +++ b/include/ruby/internal/core/rtypeddata.h @@ -282,7 +282,7 @@ struct rb_data_type_struct { * ::rb_data_type_struct::dmark, you need to update references to Ruby * objects inside of your structs. * - * @see rb_gc_location() + * @see rb_gc_update_moved(), rb_gc_location() * @warning This is called during GC runs. Object allocations are * impossible at that moment (that is why GC runs). */ diff --git a/include/ruby/internal/gc.h b/include/ruby/internal/gc.h index 1900b48a75ef9c..8986efd0e90e8d 100644 --- a/include/ruby/internal/gc.h +++ b/include/ruby/internal/gc.h @@ -187,7 +187,8 @@ void rb_gc_mark(VALUE obj); * your struct in the first place. But if that is not possible, use this * function from your ::rb_data_type_struct::dmark then. This way objects * marked using it are considered movable. If you chose this way you have to - * manually fix up locations of such moved pointers using rb_gc_location(). + * manually fix up locations of such moved pointers using rb_gc_update_moved() + * or rb_gc_location(). * * @see Bartlett, Joel F., "Compacting Garbage Collection with Ambiguous * Roots", ACM SIGPLAN Lisp Pointers Volume 1 Issue 6 pp. 3-12, @@ -209,6 +210,18 @@ void rb_gc_mark_movable(VALUE obj); */ VALUE rb_gc_location(VALUE obj); +/** + * Updates a reference to a possibly moved object. This is the same as doing + * `struct->field = rb_gc_location(struct->field)`, except it does not write to + * `struct->field` when the object did not move. Prefer this over the + * assignment, which dirties the page it writes to, and so un-shares it from a + * forked parent process. + * + * @param[in,out] ptr A place holding an object, possibly already moved. + * @post `*ptr` holds the current location of the object. + */ +void rb_gc_update_moved(VALUE *ptr); + /** * Triggers a GC process. This was the only GC entry point that we had at the * beginning. Over time our GC evolved. Now what this function does is just a diff --git a/include/ruby/ractor.h b/include/ruby/ractor.h index 8cfca2162107c8..afed7627f2a1fa 100644 --- a/include/ruby/ractor.h +++ b/include/ruby/ractor.h @@ -221,6 +221,26 @@ VALUE rb_ractor_make_shareable(VALUE obj); */ VALUE rb_ractor_make_shareable_copy(VALUE obj); +/** + * Marks the passed object as shareable, without any check. This is for + * objects which the caller knows are safe to be shared among Ractors, for + * instance because they are frozen and only refer to shareable objects. + * + * @param[out] obj Arbitrary ruby object, except special constants. + * @return Passed `obj`. + * @post Multiple Ractors can share `obj`. + * @warning No check is done. Marking an unsafe object as shareable + * breaks the Ractor isolation. + * @warning Do not make objects shareable more than needed. A shareable + * object is out of the reach of a local GC, so it is not + * collected until a global GC runs. This is for the few objects + * which live as long as the process, typically internal + * constants, not for objects created per call. + * @note Do not call this directly; use #RB_OBJ_SET_SHAREABLE, or + * #RB_OBJ_SET_FROZEN_SHAREABLE to freeze at the same time. + */ +VALUE rb_obj_set_shareable(VALUE obj); + RBIMPL_SYMBOL_EXPORT_END() /** @@ -262,11 +282,21 @@ rb_ractor_shareable_p(VALUE obj) } // TODO: optimize on interpreter core + +/** + * Wrapper of rb_obj_set_shareable(). Use this macro, not the function. + */ #ifndef RB_OBJ_SET_SHAREABLE -VALUE rb_obj_set_shareable(VALUE obj); // ractor.c #define RB_OBJ_SET_SHAREABLE(obj) rb_obj_set_shareable(obj) #endif +/** + * Freezes and marks the object as shareable. The same warning and note as + * rb_obj_set_shareable() apply. + * + * @param[out] obj Arbitrary ruby object, except special constants. + * @return Passed `obj`. + */ static inline VALUE RB_OBJ_SET_FROZEN_SHAREABLE(VALUE obj) { diff --git a/internal/gc.h b/internal/gc.h index 19202be1232863..cfc04e42da1804 100644 --- a/internal/gc.h +++ b/internal/gc.h @@ -230,8 +230,9 @@ void rb_gc_after_fork(rb_pid_t pid); if (_obj != (VALUE)*(ptr)) *(ptr) = (void *)_obj; \ } while (0) -#define rb_gc_move_ptr(ptr) do { \ - VALUE _obj = rb_gc_location((VALUE)*(ptr)); \ +#define rb_gc_update_moved_ptr(ptr) do { \ + VALUE _obj = (VALUE)*(ptr); \ + rb_gc_update_moved(&_obj); \ if (_obj != (VALUE)*(ptr)) *(ptr) = (void *)_obj; \ } while (0) @@ -308,6 +309,13 @@ bool rb_gc_multi_objspace_p(void); bool rb_gc_obj_foreign_p(VALUE obj); void *rb_gc_objspace_alloc(void); void rb_gc_objspace_retire_gc(void); +/* Build an object, or suppress GC, in a named objspace rather than the current + * Ractor's. Only create_ractor_alloc_thread() needs these. */ +VALUE rb_data_typed_object_wrap_in_objspace(void *objspace, VALUE klass, void *datap, const rb_data_type_t *type); +VALUE rb_data_typed_object_zalloc_in_objspace(void *objspace, VALUE klass, size_t size, const rb_data_type_t *type); +VALUE rb_gc_objspace_disable_no_rest(void *objspace); +VALUE rb_gc_objspace_enable(void *objspace); + void rb_gc_objspace_retire(void **objspace_slot); void rb_gc_objspace_postmortem_self(void); void rb_gc_objspace_absorb_into_current(void **objspace_slot); diff --git a/io_buffer.c b/io_buffer.c index e05d4119251103..4acf76ac1fa266 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -304,7 +304,7 @@ rb_io_buffer_type_compact(void *_buffer) // The `source` String has to be pinned, because the `base` may point to the embedded String content, // which can be otherwise moved by GC compaction. } else { - buffer->source = rb_gc_location(buffer->source); + rb_gc_update_moved(&buffer->source); } } } diff --git a/iseq.c b/iseq.c index 885e1f2aa2a923..f81e244afeefe9 100644 --- a/iseq.c +++ b/iseq.c @@ -436,7 +436,7 @@ rb_iseq_mark_and_move(rb_iseq_t *iseq, bool reference_updating) } if (reference_updating) { - rb_gc_move_ptr(&cds[i].cc); + rb_gc_update_moved_ptr(&cds[i].cc); } else { if (cc_is_active(cc)) { diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb index ca7bb0719aac24..6a1de4efc6baf8 100644 --- a/lib/bundler/version.rb +++ b/lib/bundler/version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false module Bundler - VERSION = "4.1.0.dev".freeze + VERSION = "4.1.0.beta1".freeze def self.bundler_major_version @bundler_major_version ||= gem_version.segments.first diff --git a/lib/pathname.rb b/lib/pathname.rb index c323dfc7e32c7c..1fe1b022d4db99 100644 --- a/lib/pathname.rb +++ b/lib/pathname.rb @@ -88,9 +88,9 @@ class Pathname # * FileUtils * # :markup: markdown # # call-seq: - # rmtree -> 0 + # rmtree(noop: nil, verbose: nil, secure: nil) -> self # - # Deletes the entire filetree at the path in `self`; returns `0`: + # Deletes the entire filetree at the path in `self`; returns `self`: # # ```ruby # dir_pn = Pathname('foo/bar/baz') # => # @@ -103,6 +103,8 @@ class Pathname # * FileUtils * # # Use method #rmdir to delete a single (empty) directory. # + # See FileUtils.rm_rf for keyword arguments. + # def rmtree(noop: nil, verbose: nil, secure: nil) # The name "rmtree" is borrowed from File::Path of Perl. # File::Path provides "mkpath" and "rmtree". diff --git a/lib/rubygems.rb b/lib/rubygems.rb index 0ff3beaea1f83c..85b4055951648f 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -9,7 +9,7 @@ require "rbconfig" module Gem - VERSION = "4.1.0.dev" + VERSION = "4.1.0.beta1" end require_relative "rubygems/defaults" diff --git a/load.c b/load.c index 8be85c60047b2f..c6b151abd3cec9 100644 --- a/load.c +++ b/load.c @@ -731,6 +731,46 @@ realpath_internal_cached(VALUE hash, VALUE path) return realpath; } +struct load_prism_args { + pm_parse_result_t result; + VALUE fname; + VALUE realpath_map; + const rb_iseq_t *iseq; + VALUE error; +}; + +static VALUE +load_prism_parse(VALUE args_ptr) +{ + struct load_prism_args *args = (struct load_prism_args *)args_ptr; + pm_parse_result_t *result = &args->result; + VALUE fname = args->fname; + + VALUE error = pm_load_parse_file(result, fname, NULL); + if (error != Qnil) { + args->error = error; + return Qnil; + } + + int error_state; + args->iseq = pm_iseq_new_top(&result->node, rb_fstring_lit(""), fname, + realpath_internal_cached(args->realpath_map, fname), NULL, &error_state); + if (error_state) { + RUBY_ASSERT(args->iseq == NULL); + rb_jump_tag(error_state); + } + + return Qnil; +} + +static VALUE +load_prism_free_result(VALUE args_ptr) +{ + struct load_prism_args *args = (struct load_prism_args *)args_ptr; + pm_parse_result_free(&args->result); + return Qnil; +} + static inline void load_iseq_eval(rb_execution_context_t *ec, VALUE fname) { @@ -744,29 +784,27 @@ load_iseq_eval(rb_execution_context_t *ec, VALUE fname) VALUE realpath_map = box->loaded_features_realpath_map; if (rb_ruby_prism_p()) { - pm_parse_result_t result; - pm_parse_result_init(&result); - result.node.coverage_enabled = 1; - - VALUE error = pm_load_parse_file(&result, fname, NULL); - - if (error == Qnil) { - int error_state; - iseq = pm_iseq_new_top(&result.node, rb_fstring_lit(""), fname, realpath_internal_cached(realpath_map, fname), NULL, &error_state); - - pm_parse_result_free(&result); - - if (error_state) { - RUBY_ASSERT(iseq == NULL); - rb_jump_tag(error_state); - } - } - else { + struct load_prism_args args = { + .fname = fname, + .realpath_map = realpath_map, + .iseq = NULL, + .error = Qnil, + }; + pm_parse_result_init(&args.result); + args.result.node.coverage_enabled = 1; + + /* The parse result must be freed even if parsing or compiling + * raises (e.g. an asynchronously raised IOError while reading a + * pipe), so wrap it in rb_ensure. */ + rb_ensure(load_prism_parse, (VALUE)&args, load_prism_free_result, (VALUE)&args); + + if (args.error != Qnil) { rb_vm_pop_frame(ec); RB_GC_GUARD(v); - pm_parse_result_free(&result); - rb_exc_raise(error); + rb_exc_raise(args.error); } + + iseq = args.iseq; } else { rb_ast_t *ast; diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 39ff95fd3b47c0..d7641cb4a7ae48 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2661,18 +2661,16 @@ def sticky?() FileTest.sticky?(@path) end # call-seq: # symlink? -> true or false # - # Returns whether the entry at the path in `self` is a symbolic link: + # Returns whether the entry at the path in `self` + # is a [symbolic link](rdoc-ref:file/symbolic_links.md): # # ```ruby - # # Create Pathnames. - # file_pn = Pathname('doc/extension.rdoc') # => # - # target_pn = Pathname('..').join(file_pn) # => # - # link_pn = Pathname('lib/u.tmp') # => # - # link_pn.symlink? # => false - # # Create link. - # link_pn.make_symlink(target_pn) - # link_pn.symlink? # => true - # link_pn.delete # Clean up. + # file_pn = Pathname('README.md') + # link_pn = Pathname('foo') + # link_pn.make_symlink(file_pn) + # file_pn.symlink? # => false + # link_pn.symlink? # => true + # link_pn.unlink # Clean up. # ``` # def symlink?() FileTest.symlink?(@path) end diff --git a/ractor.c b/ractor.c index 6d60d6e6af6869..3e610ccaaaa97d 100644 --- a/ractor.c +++ b/ractor.c @@ -2528,16 +2528,46 @@ move_neutralize_source(VALUE obj) bool wipe_body = true; switch (BUILTIN_TYPE(obj)) { case T_STRING: + if (!STR_EMBED_P(obj) && !rb_str_reembeddable_p(obj)) { + /* A heap (non-embedded), shared root string keeps its buffer because + * other strings reference this shared root. It needs to keep T_STRING + * because otherwise the GC will not free the buffer when this object + * dies which will leak memory. */ + RBASIC_SET_CLASS_RAW(obj, rb_cRactorMovedObject); + RBASIC(obj)->flags |= FL_FREEZE; + RBASIC_SET_FULL_SHAPE_ID(obj, (shape_id & ~SHAPE_ID_LAYOUT_MASK) | SHAPE_ID_LAYOUT_OTHER); + RSTRING(obj)->len = 0; + return; + } wipe_body = !rb_str_embedded_shared_root_p(obj); break; case T_ARRAY: + if (!ARY_EMBED_P(obj) && !ARY_SHARED_P(obj) && (ARY_SHARED_ROOT_P(obj) || OBJ_FROZEN(obj))) { + /* A heap (non-embedded), shared root array keeps its buffer because + * other arrays reference this shared root. It needs to keep T_ARRAY + * because otherwise the GC will not free the buffer when this object + * dies which will leak memory. */ + RBASIC_SET_CLASS_RAW(obj, rb_cRactorMovedObject); + RBASIC(obj)->flags |= FL_FREEZE; + RBASIC_SET_FULL_SHAPE_ID(obj, (shape_id & ~SHAPE_ID_LAYOUT_MASK) | SHAPE_ID_LAYOUT_OTHER); + if (!ARY_SHARED_ROOT_P(obj)) { + /* Present as empty to stale readers. Not for a shared root: its + * len doubles as the buffer capacity that ARY_HEAP_SIZE frees by. */ + RARRAY(obj)->as.heap.len = 0; + } + return; + } wipe_body = !rb_ary_embedded_shared_root_p(obj); break; default: break; } - VALUE flags = T_OBJECT | FL_FREEZE | (RBASIC(obj)->flags & FL_PROMOTED); + /* Keep FL_FINALIZE: the finalizer table entry stays keyed on this slot, and a + * shell without the flag makes the two disagree (rb_gc_impl_shutdown_call_finalizer_i + * asserts on it). The finalizer runs when the shell dies, in the Ractor that + * defined it; the rebuilt object gets fresh flags and does not inherit it. */ + VALUE flags = T_OBJECT | FL_FREEZE | (RBASIC(obj)->flags & (FL_PROMOTED | FL_FINALIZE)); /* Read the slot size before the header is rewritten. */ size_t slot_size = rb_gc_obj_slot_size(obj); RBASIC_SET_CLASS_RAW(obj, rb_cRactorMovedObject); diff --git a/ractor.rb b/ractor.rb index e826e61b37655a..67d96cbbd112ee 100644 --- a/ractor.rb +++ b/ractor.rb @@ -297,6 +297,9 @@ def self.count # # r1 done # # r0 done # + # Closing one of the given ports raises Ractor::ClosedError, whether it was + # closed before the call or while it waits. + # # The following example is almost equivalent to ractors.map(&:value) except the thread # is unblocked when any of the ractors has terminated as opposed to waiting for their termination in # the array element order. @@ -759,12 +762,18 @@ class Port # is already there and returns +nil+ otherwise. # # If the port is closed and there are no more messages in the message queue, - # the method raises Ractor::ClosedError. + # the method raises Ractor::ClosedError. Closing a port while this method + # waits on it ends the wait the same way; messages queued before the close + # are received first. # # port = Ractor::Port.new # port.close # port.receive #=> raise Ractor::ClosedError # + # port = Ractor::Port.new + # Thread.new { sleep 0.1; port.close } + # port.receive #=> raise Ractor::ClosedError, after 0.1 seconds + # def receive(timeout: nil) __builtin_cexpr! %q{ ractor_port_receive(ec, self, timeout) @@ -823,6 +832,11 @@ def send obj, move: false # Closes the port. Sending to a closed port is prohibited. # Receiving is also prohibited if there are no messages in its message queue. # + # Messages already in the queue are kept: a receiver takes them before the + # port reports itself closed. A Ractor::Port#receive waiting on the port when + # it closes stops there and raises Ractor::ClosedError, rather than waiting + # for a message that can no longer arrive. + # # Only the Ractor which created the port is allowed to close it. # # port = Ractor::Port.new diff --git a/ractor_core.h b/ractor_core.h index 6a545251473dc5..a3e2ce2f54af95 100644 --- a/ractor_core.h +++ b/ractor_core.h @@ -173,7 +173,7 @@ enum ractor_wakeup_status { wakeup_none, wakeup_by_send, wakeup_by_interrupt, - // wakeup_by_close, + wakeup_by_close, }; struct ractor_waiter { diff --git a/ractor_sync.c b/ractor_sync.c index c6e4d8a6d31df1..3cc61f52d19706 100644 --- a/ractor_sync.c +++ b/ractor_sync.c @@ -428,10 +428,13 @@ ractor_queue_size(const struct ractor_queue *rq) return size; } -static void +// Returns whether this call is the one that closed it. +static bool ractor_queue_close(struct ractor_queue *rq) { + bool closed_now = !rq->closed; rq->closed = true; + return closed_now; } static void @@ -536,8 +539,27 @@ ractor_add_port(rb_ractor_t *r, st_data_t id) // The table is full. Rebuild it outside of the ractor lock (mutators // are serialized by the per-ractor GVL) and swap it under the lock // to exclude the readers (other ractors). - st_table *const new_tab = st_copy(old_tab); - st_insert(new_tab, id, (st_data_t)rq); + st_table *new_tab; + + // Those allocations can run a global GC, whose reap frees dead ports and + // drops them from old_tab; a copy taken across one would republish the + // freed queues. Only the owner inserts into its own table, so a changed + // count means a reap ran. Check after each allocation: st_copy fills the + // header before it allocates the entries, so a reap in between leaves the + // copy counting rows it does not have, which st_insert must not be given. + while (1) { + const st_index_t entries = st_table_size(old_tab); + + new_tab = st_copy(old_tab); + + if (st_table_size(old_tab) == entries) { + st_insert(new_tab, id, (st_data_t)rq); + + if (st_table_size(old_tab) == entries) break; + } + + st_free_table(new_tab); + } RACTOR_LOCK(r); { @@ -612,18 +634,21 @@ ractor_closed_port_p(rb_execution_context_t *ec, rb_ractor_t *r, const struct ra static void ractor_deliver_incoming_messages(rb_execution_context_t *ec, rb_ractor_t *cr); static bool ractor_queue_empty_p(rb_ractor_t *r, const struct ractor_queue *rq); +static bool ractor_wakeup_all(rb_ractor_t *r, enum ractor_wakeup_status wakeup_status); + static bool ractor_close_port(rb_execution_context_t *ec, rb_ractor_t *cr, const struct ractor_port *rp) { VM_ASSERT(cr == rp->r); struct ractor_queue *rq = NULL; + bool closed_now = false; RACTOR_LOCK_SELF(cr); { ractor_deliver_incoming_messages(ec, cr); // check incoming messages if (st_lookup(rp->r->sync.ports, ractor_port_id(rp), (st_data_t *)&rq)) { - ractor_queue_close(rq); + closed_now = ractor_queue_close(rq); if (ractor_queue_empty_p(cr, rq)) { // delete from the table @@ -635,6 +660,12 @@ ractor_close_port(rb_execution_context_t *ec, rb_ractor_t *cr, const struct ract } RACTOR_UNLOCK_SELF(cr); + if (closed_now) { + // Only when this call closed it: waking the Ractor is not free to the + // other waiters, and a re-close of a closed port is news to nobody. + ractor_wakeup_all(cr, wakeup_by_close); + } + return rq != NULL; } @@ -1281,7 +1312,7 @@ wakeup_status_str(enum ractor_wakeup_status wakeup_status) case wakeup_none: return "none"; case wakeup_by_send: return "by_send"; case wakeup_by_interrupt: return "by_interrupt"; - // case wakeup_by_close: return "by_close"; + case wakeup_by_close: return "by_close"; } rb_bug("unreachable"); } @@ -1441,6 +1472,14 @@ ractor_check_received(rb_ractor_t *cr, struct ractor_queue *messages) // Returns false if the deadline `end` passed with nothing to deliver. Incoming // messages are delivered even then, so the caller retries its queue once more. +// A wait can end on a wakeup meant for another port, so the caller keeps the +// deadline: a stream of them must not hold a timed receive past its time. +static bool +ractor_deadline_passed_p(const rb_hrtime_t *end) +{ + return end != NULL && rb_hrtime_now() >= *end; +} + static bool ractor_wait_receive(rb_execution_context_t *ec, rb_ractor_t *cr, const rb_hrtime_t *end) { @@ -1525,6 +1564,11 @@ ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp, const r else if (!ractor_wait_receive(ec, cr, end)) { return Qundef; } + else if (ractor_deadline_passed_p(end)) { + // The wait ended on a wakeup meant for another port, which says + // nothing about the clock. One more look, then the deadline stands. + return ractor_try_receive(ec, cr, rp); + } } } @@ -1832,6 +1876,10 @@ ractor_selector__wait(rb_execution_context_t *ec, VALUE selector, const rb_hrtim else if (!ractor_wait_receive(ec, cr, end)) { return Qnil; } + else if (ractor_deadline_passed_p(end)) { + st_foreach(s->ports, ractor_selector_wait_i, (st_data_t)&data); + return data.found ? rb_ary_new_from_args(2, data.rpv, data.v) : Qnil; + } } } diff --git a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock index a08089a6f7b33b..9bf8b9585bc173 100644 --- a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock +++ b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock @@ -46,4 +46,4 @@ DEPENDENCIES tapioca BUNDLED WITH - 4.1.0.dev + 4.1.0.beta1 diff --git a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock index 05f3bc4e3f1d86..9e6c2fe23d7df6 100644 --- a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock +++ b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock @@ -32,4 +32,4 @@ DEPENDENCIES warbler (~> 2.1) BUNDLED WITH - 4.1.0.dev + 4.1.0.beta1 diff --git a/st.c b/st.c index 3fa125bbf72658..334938f3b82e16 100644 --- a/st.c +++ b/st.c @@ -596,6 +596,13 @@ st_init_existing_table_with_size(st_table *tab, const struct st_hash_type *type, tab->bin_power = features[n].bin_power; tab->size_ind = features[n].size_ind; + /* The table may be embedded in an object the GC can already reach (T_HASH, + imemo_cdhash), in which case the allocation below can mark it. Empty it + first, marking walks entries[entries_start..entries_bound). */ + tab->entries = NULL; + tab->num_entries = 0; + tab->entries_start = tab->entries_bound = 0; + size_t memsize = get_allocated_entries(tab) * sizeof(st_table_entry); if (tab->entry_power > MAX_POWER2_FOR_TABLES_WITHOUT_BINS) { memsize += bins_size(tab); diff --git a/symbol.c b/symbol.c index 32276492d215e0..a6b5741b45e004 100644 --- a/symbol.c +++ b/symbol.c @@ -197,7 +197,7 @@ sym_id_entry_list_compact(void *ptr) struct sym_id_entry *entry; rb_darray_foreach(ary, i, entry) { - entry->str = rb_gc_location(entry->str); + rb_gc_update_moved(&entry->str); } } @@ -251,7 +251,7 @@ id_entry_dir_compact(void *ptr) struct id_entry_dir *dir = ptr; for (long i = 0; i < dir->capa; i++) { if (dir->entries[i]) { - dir->entries[i] = rb_gc_location(dir->entries[i]); + rb_gc_update_moved(&dir->entries[i]); } } } diff --git a/template/Makefile.in b/template/Makefile.in index 407600869d8b60..a519dfb858c6ea 100644 --- a/template/Makefile.in +++ b/template/Makefile.in @@ -228,6 +228,7 @@ PREP = @PREP@ ARCHFILE = @ARCHFILE@ SETUP = EXTSTATIC = @EXTSTATIC@ +PROGRAM_EXTS = @PROGRAM_EXTS@ ENCSTATIC = @ENCSTATIC@ SET_LC_MESSAGES = env LC_MESSAGES=C @@ -317,11 +318,16 @@ miniruby$(EXEEXT): $(Q) $(PURIFY) $(CC) $(EXE_LDFLAGS) $(XLDFLAGS) $(NORMALMAINOBJ) $(MINIOBJS) $(COMMONOBJS) $(MAINLIBS) $(OUTFLAG)$@ $(Q) $(POSTLINK) +# With statically linked extensions, only the sub-make from exts.mk, +# which passes the archives, libraries and EXTINITS and empties +# PROGRAM_EXTS, can link $(PROGRAM); the top-level $(PROGRAM) just +# depends on `exts` (see common.mk). $(PROGRAM): - @$(RM) $@ - $(ECHO) linking $@ - $(Q) $(PURIFY) $(CC) $(EXE_LDFLAGS) $(XLDFLAGS) $(MAINOBJ) $(EXTOBJS) $(LIBRUBYARG) $(MAINLIBS) $(EXTLIBS) $(OUTFLAG)$@ - $(Q) $(POSTLINK) + $(Q) if test -z "$(PROGRAM_EXTS)"; then \ + $(RM) $@ && $(ECHO0) linking $@ && \ + $(PURIFY) $(CC) $(EXE_LDFLAGS) $(XLDFLAGS) $(MAINOBJ) $(EXTOBJS) $(LIBRUBYARG) $(MAINLIBS) $(EXTLIBS) $(OUTFLAG)$@ && \ + { $(POSTLINK); }; \ + fi $(PROGRAM): @XRUBY_LIBPATHENV_WRAPPER@ diff --git a/test/erb/test_erb.rb b/test/erb/test_erb.rb index 6da25146ff7966..c789e074510df5 100644 --- a/test/erb/test_erb.rb +++ b/test/erb/test_erb.rb @@ -50,7 +50,7 @@ def test_html_escape assert_equal("", ERB::Util.html_escape("")) assert_equal("abc", ERB::Util.html_escape("abc")) assert_equal("<<", ERB::Util.html_escape("<\<")) - assert_equal("'&"><", ERB::Util.html_escape("'&\"><")) + assert_equal("'&"><" * 10, ERB::Util.html_escape("'&\"><" * 10)) assert_equal("", ERB::Util.html_escape(nil)) assert_equal("123", ERB::Util.html_escape(123)) diff --git a/test/ruby/test_hash.rb b/test/ruby/test_hash.rb index 109b6334bee8c7..8da9d010ec9418 100644 --- a/test/ruby/test_hash.rb +++ b/test/ruby/test_hash.rb @@ -1327,6 +1327,11 @@ def test_merge assert_equal({1=>8, 2=>4, 3=>4, 5=>7}, h1.merge(h2, h3) {|k, v1, v2| k + v1 + v2 }) end + def test_merge_during_gc + hash = @cls[a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8] + assert_equal(9, EnvUtil.under_gc_stress(0x04) { hash.merge(i: 9) }[:i]) + end + def test_merge_on_identhash h = @cls[1=>2,3=>4,5=>6] h.compare_by_identity diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index e435d0856c72bf..74398486713f67 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -563,6 +563,34 @@ def test_stillborn_ractor_gc RUBY end + # A thread that released the GVL keeps its EC, so it still resolves an objspace to + # charge its frees to. It must not be sent to the objspace of a child Ractor being + # built by another thread of the same Ractor: a stillborn child frees that objspace. + def test_stillborn_ractor_with_free_off_gvl + assert_ractor(<<~'RUBY', require: '-test-/gvl/call_without_gvl', timeout: 60) + x = 42 # capturing an outer local makes Ractor.new raise IsolationError + stop = false + ready = Queue.new + freers = 4.times.map do + Thread.new do + ready << :up + Bug::Thread.xfree_without_gvl(2_000) until stop + end + end + freers.size.times { ready.pop } # all of them are churning before we start + 2_000.times do + begin + Ractor.new { x } + rescue Ractor::IsolationError + end + end + stop = true + freers.each(&:join) + GC.start + GC.verify_internal_consistency + RUBY + end + # Moving a CoW shared-root String must not steal its buffer (regression guard for the # remaining sharers reading freed memory). def test_move_shared_root_string_keeps_buffer @@ -908,4 +936,26 @@ def test_port_queue_dropped_when_port_unreachable RUBY end + + def test_move_object_with_finalizer + # The moved-from shell keeps its finalizer table entry, so it has to keep + # FL_FINALIZE with it; the two disagreeing failed an assertion at shutdown. + assert_normal_exit(<<~'RUBY', '[Bug #21368]') + Warning[:experimental] = false + r = Ractor.new { Ractor.receive } + 1000.times do + o = Object.new + ObjectSpace.define_finalizer(o, proc { |id| }) + r.send(o, move: true) + end + RUBY + + assert_in_out_err(%w[-W0], <<~'RUBY', %w[sent finalized], [], '[Bug #21368]') + r = Ractor.new { Ractor.receive } + o = Object.new + ObjectSpace.define_finalizer(o, proc { |id| $stdout.puts "finalized" }) + r.send(o, move: true) + $stdout.puts "sent" + RUBY + end end diff --git a/thread.c b/thread.c index 8310529a38b941..309c409bc21582 100644 --- a/thread.c +++ b/thread.c @@ -1134,49 +1134,48 @@ rb_thread_create(VALUE (*fn)(void *), void *arg) static VALUE create_ractor_alloc_thread(rb_ractor_t *r, rb_ractor_t *cr, rb_execution_context_t *ec) { - /* Allocate the child's main Thread and root Fiber wrappers directly in the child's - * objspace, so the thread is built of objects it owns. Whole-VM walks read - * cr->objspace: swap it under the VM lock, unobservable to others. */ + /* Build the child's main Thread and root Fiber wrappers in the child's objspace, + * so the thread is made of objects it owns. Hand that objspace down rather than + * pointing cr->objspace at it: threads holding no GVL read that slot to charge + * their frees, and one of them must never be sent to a heap a stillborn child is + * about to free. + * + * The child's objspace is not in vm->ractor.set yet, so cover it through its + * creator before the first allocation and keep the cover until vm_insert_ractor + * clears it under the VM lock. One slot suffices: one Ractor creates children + * serially. */ + void *const child_objspace = r->objspace; volatile VALUE thval = Qundef; const bool multi_objspace = rb_gc_multi_objspace_p(); enum ruby_tag_type alloc_state = TAG_NONE; RB_VM_LOCKING() { - void *const parent_objspace = cr->objspace; - if (multi_objspace) cr->objspace = r->objspace; - /* The wrapper allocations must not re-enter GC: while cr->objspace points at - * the child, the creator's own objspace is invisible to every walk, so a global - * GC would skip it and leave stale mark bits (a UAF). Single allocations; - * suppressing GC costs only a little growth. */ - VALUE gc_was_disabled = rb_gc_local_disable_no_rest(); - /* The alloc can raise NoMemoryError; a longjmp here would skip both the unlock - * of RB_VM_LOCKING and the objspace restore, so catch and rethrow outside. */ + if (multi_objspace) { + RUBY_ASSERT(cr->creating_child_objspace == NULL); + cr->creating_child_objspace = child_objspace; + } + /* Suppress the child's GC, not the creator's: a cycle here would collect a + * half-built child. Single allocations; this costs only a little growth. */ + VALUE gc_was_disabled = rb_gc_objspace_disable_no_rest(child_objspace); + /* The alloc can raise NoMemoryError; a longjmp here would skip the unlock of + * RB_VM_LOCKING, so catch and rethrow outside. */ EC_PUSH_TAG(ec); if ((alloc_state = EC_EXEC_TAG()) == TAG_NONE) { - thval = rb_thread_alloc(rb_cThread); + thval = rb_thread_alloc_in_objspace(rb_cThread, child_objspace); } EC_POP_TAG(); - if (gc_was_disabled == Qfalse) rb_gc_local_enable(); - if (multi_objspace) cr->objspace = parent_objspace; - /* The child's objspace holds the wrappers but is not in vm->ractor.set yet: - * keep it enumerable until vm_insert_ractor clears this under the VM lock. One - * slot suffices: the GVL is never released between set and clear and one - * Ractor creates children serially, so no overwrite (asserted: releasing the - * GVL here in the future would break it). */ - if (alloc_state == TAG_NONE && multi_objspace) { - RUBY_ASSERT(cr->creating_child_objspace == NULL); - cr->creating_child_objspace = r->objspace; - } - } - if (alloc_state != TAG_NONE) { - /* No cover was set; park the child objspace for the orphan merge and re-raise. */ - RB_VM_LOCKING() { + if (gc_was_disabled == Qfalse) rb_gc_objspace_enable(child_objspace); + if (alloc_state != TAG_NONE) { + /* Drop the cover and park the objspace in this same section: between two of + * them another Ractor's global GC would find a populated objspace that is + * neither covered nor a zombie. */ + if (multi_objspace) cr->creating_child_objspace = NULL; if (r->objspace) { rb_gc_objspace_disown(r->objspace); r->objspace = NULL; } } - EC_JUMP_TAG(ec, alloc_state); } + if (alloc_state != TAG_NONE) EC_JUMP_TAG(ec, alloc_state); return thval; } diff --git a/thread_sched.c b/thread_sched.c index 7bceb1f66cc934..e02d97a9c93900 100644 --- a/thread_sched.c +++ b/thread_sched.c @@ -109,13 +109,6 @@ static void timer_thread_wakeup_force(void); // 1 = the main Ractor's threads too; 2 = the main thread as well. static int mn_threads_mode = 0; -// Only consulted from USE_MN_THREADS code; the platform gate is not defined yet here. -static bool -mn_threads_enabled_p(void) -{ - return mn_threads_mode >= 0; -} - static void nt_snts_join(rb_vm_t *vm, struct rb_native_thread *nt); static void nt_snts_leave(rb_vm_t *vm, struct rb_native_thread *nt); static bool nt_shared_loop(struct rb_native_thread *nt); diff --git a/thread_sched_mn.c b/thread_sched_mn.c index 73a195c557f3ae..9f66d0efc37371 100644 --- a/thread_sched_mn.c +++ b/thread_sched_mn.c @@ -919,6 +919,11 @@ nt_free_stack(void *mstack) rb_native_mutex_unlock(&nt_machine_stack_lock); } +static bool +mn_threads_enabled_p(void) +{ + return mn_threads_mode >= 0; +} static int native_thread_check_and_create_shared(rb_vm_t *vm) diff --git a/thread_sync.c b/thread_sync.c index 07a578bf56b764..ca8a2217bab2e7 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -1274,7 +1274,7 @@ static void monitor_compact(void *ptr) { struct rb_monitor *mc = ptr; - mc->mutex = rb_gc_location(mc->mutex); + rb_gc_update_moved(&mc->mutex); } static const rb_data_type_t monitor_data_type = { diff --git a/tool/bundler/dev_gems.rb.lock b/tool/bundler/dev_gems.rb.lock index 72916fb1e1d95f..0e715aa3c5599d 100644 --- a/tool/bundler/dev_gems.rb.lock +++ b/tool/bundler/dev_gems.rb.lock @@ -132,4 +132,4 @@ CHECKSUMS turbo_tests (2.2.5) sha256=3fa31497d12976d11ccc298add29107b92bda94a90d8a0a5783f06f05102509f BUNDLED WITH - 4.1.0.dev + 4.1.0.beta1 diff --git a/tool/bundler/rubocop_gems.rb.lock b/tool/bundler/rubocop_gems.rb.lock index f265e7c9ebd52b..b0b2899f3f8d89 100644 --- a/tool/bundler/rubocop_gems.rb.lock +++ b/tool/bundler/rubocop_gems.rb.lock @@ -157,4 +157,4 @@ CHECKSUMS unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f BUNDLED WITH - 4.1.0.dev + 4.1.0.beta1 diff --git a/tool/bundler/standard_gems.rb.lock b/tool/bundler/standard_gems.rb.lock index 8ef7806bcc80e6..9b3ecaa36a9057 100644 --- a/tool/bundler/standard_gems.rb.lock +++ b/tool/bundler/standard_gems.rb.lock @@ -177,4 +177,4 @@ CHECKSUMS unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f BUNDLED WITH - 4.1.0.dev + 4.1.0.beta1 diff --git a/tool/bundler/test_gems.rb.lock b/tool/bundler/test_gems.rb.lock index 0b9ac341625e3b..9ca68b3d9883ff 100644 --- a/tool/bundler/test_gems.rb.lock +++ b/tool/bundler/test_gems.rb.lock @@ -99,4 +99,4 @@ CHECKSUMS tilt (2.7.0) sha256=0d5b9ba69f6a36490c64b0eee9f6e9aad517e20dcc848800a06eb116f08c6ab3 BUNDLED WITH - 4.1.0.dev + 4.1.0.beta1 diff --git a/variable.c b/variable.c index b703819e2affdb..2ff49546c8b2d6 100644 --- a/variable.c +++ b/variable.c @@ -689,13 +689,8 @@ rb_gvar_val_compactor(void *_var) { struct rb_global_variable *var = (struct rb_global_variable *)_var; - VALUE obj = (VALUE)var->data; - - if (obj) { - VALUE new = rb_gc_location(obj); - if (new != obj) { - var->data = (void*)new; - } + if (var->data) { + rb_gc_update_moved_ptr(&var->data); } } diff --git a/vm.c b/vm.c index 23bdefdabd5527..d1413549a8a7e9 100644 --- a/vm.c +++ b/vm.c @@ -3399,9 +3399,9 @@ rb_vm_update_references(void *ptr) if (ptr) { rb_vm_t *vm = ptr; - vm->self = rb_gc_location(vm->self); - vm->orig_progname = rb_gc_location(vm->orig_progname); - vm->cc_refinement_set = rb_gc_location(vm->cc_refinement_set); + rb_gc_update_moved(&vm->self); + rb_gc_update_moved(&vm->orig_progname); + rb_gc_update_moved(&vm->cc_refinement_set); if (vm->root_box) rb_box_gc_update_references(vm->root_box); @@ -3411,9 +3411,9 @@ rb_vm_update_references(void *ptr) rb_gc_update_values(RUBY_NSIG, vm->trap_list.cmd); if (vm->coverages) { - vm->coverages = rb_gc_location(vm->coverages); - vm->cme2counter = rb_gc_location(vm->cme2counter); - vm->me_set = rb_gc_location(vm->me_set); + rb_gc_update_moved(&vm->coverages); + rb_gc_update_moved(&vm->cme2counter); + rb_gc_update_moved(&vm->me_set); } } } @@ -3785,17 +3785,13 @@ rb_execution_context_update(rb_execution_context_t *ec) // safely use rb_gc_location on such slots. if (!rb_zjit_enabled_p) { for (i = 0; i < (long)(sp - p); i++) { - VALUE ref = p[i]; - VALUE update = rb_gc_location(ref); - if (ref != update) { - p[i] = update; - } + rb_gc_update_moved(&p[i]); } } while (cfp != limit_cfp) { const VALUE *ep = cfp->ep; - cfp->self = rb_gc_location(cfp->self); + rb_gc_update_moved(&cfp->self); if (CFP_ZJIT_FRAME_P(cfp)) { const zjit_jit_frame_t *jit_frame = CFP_ZJIT_FRAME(cfp); rb_zjit_jit_frame_update_references((zjit_jit_frame_t *)jit_frame); @@ -3804,23 +3800,23 @@ rb_execution_context_update(rb_execution_context_t *ec) // was initialized by ZJIT and may have been written later by // vm_caller_setup_arg_block (ISEQ frames) or rb_iterate0 (C frames). if (!jit_frame->materialize_block_code) { - cfp->block_code = (void *)rb_gc_location((VALUE)cfp->block_code); + rb_gc_update_moved_ptr(&cfp->block_code); } } else { - cfp->_iseq = (rb_iseq_t *)rb_gc_location((VALUE)cfp->_iseq); - cfp->block_code = (void *)rb_gc_location((VALUE)cfp->block_code); + rb_gc_update_moved_ptr(&cfp->_iseq); + rb_gc_update_moved_ptr(&cfp->block_code); } if (!VM_ENV_LOCAL_P(ep)) { const VALUE *prev_ep = VM_ENV_PREV_EP(ep); if (VM_ENV_FLAGS(prev_ep, VM_ENV_FLAG_ESCAPED)) { - VM_FORCE_WRITE(&prev_ep[VM_ENV_DATA_INDEX_ENV], rb_gc_location(prev_ep[VM_ENV_DATA_INDEX_ENV])); + rb_gc_update_moved((VALUE *)&prev_ep[VM_ENV_DATA_INDEX_ENV]); } if (VM_ENV_FLAGS(ep, VM_ENV_FLAG_ESCAPED)) { - VM_FORCE_WRITE(&ep[VM_ENV_DATA_INDEX_ENV], rb_gc_location(ep[VM_ENV_DATA_INDEX_ENV])); - VM_FORCE_WRITE(&ep[VM_ENV_DATA_INDEX_ME_CREF], rb_gc_location(ep[VM_ENV_DATA_INDEX_ME_CREF])); + rb_gc_update_moved((VALUE *)&ep[VM_ENV_DATA_INDEX_ENV]); + rb_gc_update_moved((VALUE *)&ep[VM_ENV_DATA_INDEX_ME_CREF]); } } @@ -3828,10 +3824,10 @@ rb_execution_context_update(rb_execution_context_t *ec) } } - ec->storage = rb_gc_location(ec->storage); + rb_gc_update_moved(&ec->storage); - ec->gen_fields_cache.obj = rb_gc_location(ec->gen_fields_cache.obj); - ec->gen_fields_cache.fields_obj = rb_gc_location(ec->gen_fields_cache.fields_obj); + rb_gc_update_moved(&ec->gen_fields_cache.obj); + rb_gc_update_moved(&ec->gen_fields_cache.fields_obj); } static enum rb_id_table_iterator_result @@ -3929,7 +3925,7 @@ rb_execution_context_mark(const rb_execution_context_t *ec) void rb_fiber_mark_self(rb_fiber_t *fib); void rb_fiber_update_self(rb_fiber_t *fib); void rb_threadptr_root_fiber_setup(rb_thread_t *th); -void rb_root_fiber_obj_setup(rb_thread_t *th); +void rb_root_fiber_obj_setup(rb_thread_t *th, void *objspace); void rb_threadptr_root_fiber_release(rb_thread_t *th); static void @@ -3937,7 +3933,7 @@ thread_compact(void *ptr) { rb_thread_t *th = ptr; - th->self = rb_gc_location(th->self); + rb_gc_update_moved(&th->self); } /* Mark the heap objects a thread owns (the caller handles ec and fiber). Split @@ -4083,10 +4079,9 @@ rb_obj_is_thread(VALUE obj) } static VALUE -thread_alloc(VALUE klass) +thread_alloc(VALUE klass, void *objspace) { - rb_thread_t *th; - return TypedData_Make_Struct(klass, rb_thread_t, &thread_data_type, th); + return rb_data_typed_object_zalloc_in_objspace(objspace, klass, sizeof(rb_thread_t), &thread_data_type); } void @@ -4198,16 +4193,22 @@ th_init(rb_thread_t *th, VALUE self, rb_vm_t *vm) } VALUE -rb_thread_alloc(VALUE klass) +rb_thread_alloc_in_objspace(VALUE klass, void *objspace) { - VALUE self = thread_alloc(klass); + VALUE self = thread_alloc(klass, objspace); rb_thread_t *target_th = rb_thread_ptr(self); target_th->ractor = GET_RACTOR(); th_init(target_th, self, target_th->vm = GET_VM()); - rb_root_fiber_obj_setup(target_th); + rb_root_fiber_obj_setup(target_th, objspace); return self; } +VALUE +rb_thread_alloc(VALUE klass) +{ + return rb_thread_alloc_in_objspace(klass, GET_RACTOR()->objspace); +} + #define REWIND_CFP(expr) do { \ rb_execution_context_t *ec__ = GET_EC(); \ VALUE *const curr_sp = (ec__->cfp++)->sp; \ @@ -4822,7 +4823,7 @@ Init_VM(void) th->top_wrapper = 0; th->top_self = rb_vm_top_self(); - rb_root_fiber_obj_setup(th); + rb_root_fiber_obj_setup(th, th->ractor->objspace); rb_vm_register_global_object((VALUE)iseq); th->ec->cfp->_iseq = iseq; diff --git a/vm_backtrace.c b/vm_backtrace.c index 19bcd7dc72a0cc..4984f8b08db85b 100644 --- a/vm_backtrace.c +++ b/vm_backtrace.c @@ -143,7 +143,7 @@ static void location_ref_update(void *ptr) { struct valued_frame_info *vfi = ptr; - vfi->btobj = rb_gc_location(vfi->btobj); + rb_gc_update_moved(&vfi->btobj); } static void @@ -798,9 +798,9 @@ backtrace_mark(void *ptr) static void location_update_entry(rb_backtrace_location_t *fi) { - fi->cme = (rb_callable_method_entry_t *)rb_gc_location((VALUE)fi->cme); + rb_gc_update_moved_ptr(&fi->cme); if (fi->iseq) { - fi->iseq = (rb_iseq_t *)rb_gc_location((VALUE)fi->iseq); + rb_gc_update_moved_ptr(&fi->iseq); } } @@ -813,8 +813,8 @@ backtrace_update(void *ptr) for (i=0; ibacktrace[i]); } - bt->strary = rb_gc_location(bt->strary); - bt->locary = rb_gc_location(bt->locary); + rb_gc_update_moved(&bt->strary); + rb_gc_update_moved(&bt->locary); } static const rb_data_type_t backtrace_data_type = { diff --git a/vm_core.h b/vm_core.h index 75d6323236169a..f2e41d633a4083 100644 --- a/vm_core.h +++ b/vm_core.h @@ -2022,6 +2022,9 @@ VM_BH_FROM_PROC(VALUE procval) /* VM related object allocate functions */ VALUE rb_thread_alloc(VALUE klass); +/* Build the Thread out of objects a named objspace owns; only a Ractor building its + * child needs this (create_ractor_alloc_thread). */ +VALUE rb_thread_alloc_in_objspace(VALUE klass, void *objspace); VALUE rb_binding_alloc(VALUE klass); VALUE rb_proc_alloc(VALUE klass, enum rb_block_type block_type); VALUE rb_proc_dup(VALUE self); diff --git a/vm_method.c b/vm_method.c index 13ec548976e0cb..e0dc349e19c235 100644 --- a/vm_method.c +++ b/vm_method.c @@ -105,11 +105,11 @@ compact_cc_entry_i(VALUE ccs_ptr, void *data) { struct rb_class_cc_entries *ccs = (struct rb_class_cc_entries *)ccs_ptr; - ccs->cme = (const struct rb_callable_method_entry_struct *)rb_gc_location((VALUE)ccs->cme); + rb_gc_update_moved_ptr(&ccs->cme); VM_ASSERT(vm_ccs_p(ccs)); for (int i=0; ilen; i++) { - ccs->entries[i].cc = (const struct rb_callcache *)rb_gc_location((VALUE)ccs->entries[i].cc); + rb_gc_update_moved_ptr(&ccs->entries[i].cc); } return ID_TABLE_CONTINUE; @@ -776,7 +776,7 @@ cc_refinement_set_compact(void *ptr) { struct cc_refinement_entries *e = ptr; for (size_t i = 0; i < e->len; i++) { - e->entries[i] = rb_gc_location(e->entries[i]); + rb_gc_update_moved(&e->entries[i]); } } diff --git a/weakmap.c b/weakmap.c index e67eb0146d4ef1..3d91a1e6112a96 100644 --- a/weakmap.c +++ b/weakmap.c @@ -91,7 +91,7 @@ wmap_compact_table_replace_i(st_data_t *k, st_data_t *v, st_data_t d, int existi { RUBY_ASSERT((VALUE)*k == rb_gc_location((VALUE)*k)); - *v = (st_data_t)rb_gc_location((VALUE)*v); + rb_gc_update_moved((VALUE *)v); return ST_CONTINUE; } @@ -579,8 +579,8 @@ wkmap_compact_table_replace(st_data_t *key_ptr, st_data_t *val_ptr, st_data_t _d { RUBY_ASSERT(existing); - *key_ptr = (st_data_t)rb_gc_location((VALUE)*key_ptr); - *val_ptr = (st_data_t)rb_gc_location((VALUE)*val_ptr); + rb_gc_update_moved((VALUE *)key_ptr); + rb_gc_update_moved((VALUE *)val_ptr); return ST_CONTINUE; } diff --git a/win32/Makefile.sub b/win32/Makefile.sub index e9a9c79e300c10..ccc3e918f509e5 100644 --- a/win32/Makefile.sub +++ b/win32/Makefile.sub @@ -476,13 +476,15 @@ COMMON_HEADERS = winsock2.h ws2tcpip.h windows.h !if "$(EXTSTATIC)" == "static" ENCOBJS = enc/encinit.$(OBJEXT) enc/libenc.lib enc/libtrans.lib -EXTOBJS = ext/extinit.$(OBJEXT) ! if !defined(ENCSTATIC) ENCSTATIC = static ! endif !else ENCOBJS = dmyenc.$(OBJEXT) +!endif EXTOBJS = dmyext.$(OBJEXT) +!if "$(EXTSTATIC)" != "" +PROGRAM_EXTS = exts !endif ext_hdrdir = $(EXTOUT)/include @@ -1149,9 +1151,14 @@ miniruby.rc: !if "$(PROGRAM)" != "" $(PROGRAM): $(MAINOBJ) $(LIBRUBY_SO) $(RUBY_INSTALL_NAME).res +! if "$(PROGRAM_EXTS)" == "" $(ECHO) linking $(@:\=/) $(Q) $(PURIFY) $(CC) $(MAINOBJ) $(EXTOBJS) $(RUBY_INSTALL_NAME).res \ $(OUTFLAG)$@ $(LIBRUBYARG) -link $(LDFLAGS) $(XLDFLAGS) +! else +# without a command nmake would infer $(PROGRAM) from ruby.c + @$(NULLCMD) +! endif !endif !if "$(WPROGRAM)" != "" @@ -1194,10 +1201,10 @@ $(LIBRUBY_SO): $(LIBRUBY_A) $(DLDOBJS) $(RUBYDEF) $(RUBY_SO_NAME).res $(LIBRUBY_DLDFLAGS) @$(RM) dummy.lib dummy.exp -$(RUBYDEF): $(LIBRUBY_A) $(RBCONFIG) +$(RUBYDEF): $(LIBRUBY_A) $(DLDOBJS) $(RBCONFIG) $(ECHO) generating $(@:\=/) $(Q) $(BOOTSTRAPRUBY_COMMAND) $(srcdir)/win32/mkexports.rb \ - -output=$@ -arch=$(ARCH) $(LIBRUBY_A) + -output=$@ -arch=$(ARCH) $(LIBRUBY_A) -- $(DLDOBJS) {$(win_srcdir)}.def.lib: $(Q) $(AR) $(ARFLAGS)$@ -def:$< diff --git a/win32/mkexports.rb b/win32/mkexports.rb index dc628314b94d37..a51ac29c6243aa 100755 --- a/win32/mkexports.rb +++ b/win32/mkexports.rb @@ -20,8 +20,14 @@ def self.create(*args, &block) klass.new(*args, &block) end + # Files after "--" contribute only the symbols they ask the linker to + # export, not everything they define. def self.extract(objs, *rest) - create(objs).exports(*rest) + dllexports = [] + if i = objs.index("--") + objs, dllexports = objs[0, i], objs[(i + 1)..-1] + end + create(objs, dllexports).exports(*rest) end def self.output(output = $output, &block) @@ -32,7 +38,7 @@ def self.output(output = $output, &block) end end - def initialize(objs) + def initialize(objs, dllexports = []) syms = {} winapis = {} syms["ruby_sysinit_real"] = "ruby_sysinit" @@ -40,6 +46,9 @@ def initialize(objs) syms[internal] = export winapis[$1] = internal if /^_?(rb_w32_\w+)(?:@\d+)?$/ =~ internal end + each_dllexport(dllexports) do |internal, export| + syms[internal] = export + end incdir = File.join(File.dirname(File.dirname(__FILE__)), "include/ruby") read_substitution(incdir+"/win32.h", syms, winapis) read_substitution(incdir+"/subst.h", syms, winapis) @@ -81,6 +90,9 @@ def forwarding(internal, export) def each_export(objs) end + def each_dllexport(objs) + end + def objdump(objs, &block) if objs.empty? $stdin.each_line(&block) @@ -134,6 +146,20 @@ def each_export(objs) yield "strcasecmp", "msvcrt.stricmp" yield "strncasecmp", "msvcrt.strnicmp" end + + def each_dllexport(objs) + return if objs.empty? + objs = objs.collect {|s| s.tr('/', '\\')} + IO.popen(%w"dumpbin -directives" + objs) do |f| + f.each do |l| + next unless /^\s*\/EXPORT:(\S+)/ =~ l + name, kind = $1.split(',', 2) + next if /^_?#{PrivateNames}/o =~ name + name.sub!(/^[@_]/, '') if /@\d+$/ !~ name + yield name, kind == "DATA" + end + end + end end class Exports::Cygwin < Exports diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 3ab73cfcd9307b..c6f2bd0d025a44 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -1485,13 +1485,6 @@ impl Interval { self.end() <= pos } - /// Check if the interval is alive at position - /// Panics if the range is not set - pub fn survives(&self, position: usize) -> bool { - assert!(self.ranges.len() > 0, "survives called on interval with no range"); - self.ranges.iter().any(|range| range.from < position && position < range.to) - } - /// Returns true if position falls inside one of the ranges in this /// interval. pub fn covers(&self, position: usize) -> bool { @@ -2703,19 +2696,28 @@ impl Assembler HashSet::default() }; - // Find survivors: intervals that survive this Call instruction - // We need to preserve the "surviving" registers past the ccall, - // so we're going to push them all on the stack, then pop - // after we make the ccall + // Find survivors: intervals that are live across this Call + // instruction. We need to preserve the "surviving" registers + // past the ccall, so we're going to push them all on the + // stack, then pop after we make the ccall + let out_vreg_id = out.is_vreg().then(|| out.vreg_idx()); + debug_assert!( + out_vreg_id.is_none_or(|id| !intervals[id].has_bounds() || intervals[id].born_at(insn_number)), + "a CCall's output interval must start at the CCall" + ); let survivors: Vec = intervals.iter() .filter(|interval| { // We need to spill register intervals on this CCall in two cases: - // 1) The VReg is referenced in an instruction after the CCall - let survives_call = interval.has_bounds() && interval.survives(insn_number); + // 1) The VReg is live across the CCall. The VReg this CCall + // defines is not one of them: its range starts here, so it + // holds no value yet and there is nothing to preserve. + let live_across_call = Some(interval.vreg_id) != out_vreg_id + && interval.covers(insn_number); + // 2) The VReg is referenced by the stack map for the CCall let stack_map_reg = stack_vreg_ids.contains(&interval.vreg_id); let is_register = interval.assigned.get().and_then(|alloc| alloc.alloc_pool_index(alloc_regs)).is_some(); - is_register && (survives_call || stack_map_reg) + is_register && (live_across_call || stack_map_reg) }) .map(|interval| interval.vreg_id) .collect(); @@ -4778,12 +4780,12 @@ mod tests { assert_eq!(interval.end(), 25); // The vreg is not live inside the hole ... - assert!(!interval.survives(10)); - assert!(!interval.survives(15)); + assert!(!interval.covers(10)); + assert!(!interval.covers(15)); // ... but the interval is not over, so it must keep its register. assert!(interval.end() > 15); // ... and it is live again on the far side. - assert!(interval.survives(22)); + assert!(interval.covers(22)); // A range that abuts the last one merges into it. interval.add_range(25, 30); @@ -4809,15 +4811,15 @@ mod tests { } #[test] - fn test_interval_survives() { + fn test_interval_covers() { let mut interval = Interval::new(VRegId(1)); interval.add_range(3, 10); - assert!(!interval.survives(2)); // Before range - assert!(!interval.survives(3)); // At start (exclusive) - assert!(interval.survives(5)); // Inside range - assert!(!interval.survives(10)); // At end (exclusive) - assert!(!interval.survives(11)); // After range + assert!(!interval.covers(2)); // Before range + assert!(interval.covers(3)); // At start (inclusive: the def position) + assert!(interval.covers(5)); // Inside range + assert!(!interval.covers(10)); // At end (exclusive) + assert!(!interval.covers(11)); // After range } #[test] @@ -4829,7 +4831,6 @@ mod tests { // so position 11 belongs to no instruction. interval.set_from(10); assert_eq!(interval.ranges, vec![LiveRange { from: 10, to: 11 }]); - assert!(!interval.survives(10)); assert!(interval.is_dead()); // With existing range, updates start but keeps end @@ -4863,13 +4864,6 @@ mod tests { interval.add_range(10, 5); } - #[test] - #[should_panic(expected = "survives called on interval with no range")] - fn test_interval_survives_panics_without_range() { - let interval = Interval::new(VRegId(1)); - interval.survives(5); - } - #[test] fn test_build_intervals() { let TestFunc { mut asm, r10, r11, r12, r13, r14, r15, .. } = build_func(); @@ -4906,7 +4900,7 @@ mod tests { ]); assert_eq!(intervals[r12_idx].start(), 20); assert_eq!(intervals[r12_idx].end(), 38); - assert!(!intervals[r12_idx].survives(32)); + assert!(!intervals[r12_idx].covers(32)); assert_eq!(intervals[r13_idx].ranges, vec![LiveRange { from: 20, to: 32 }]);