Make get/set default internal/external encoding lock-free

Also, make sure autoloading of encodings is safe across ractors.
This commit is contained in:
Luke Gruber 2025-06-30 11:20:05 -04:00 committed by John Hawthorn
parent cf4d37fbc0
commit dda5a04f2b
2 changed files with 109 additions and 79 deletions

View file

@ -26,6 +26,7 @@
#include "regenc.h" #include "regenc.h"
#include "ruby/encoding.h" #include "ruby/encoding.h"
#include "ruby/util.h" #include "ruby/util.h"
#include "ruby/atomic.h"
#include "ruby_assert.h" #include "ruby_assert.h"
#include "vm_sync.h" #include "vm_sync.h"
@ -53,6 +54,12 @@ int rb_encdb_alias(const char *alias, const char *orig);
#pragma GCC visibility pop #pragma GCC visibility pop
#endif #endif
#if ENC_DEBUG
#define encdebug(...) fprintf(stderr, __VA_ARGS__)
#else
#define encdebug(...) (void)0
#endif
static ID id_encoding; static ID id_encoding;
VALUE rb_cEncoding; VALUE rb_cEncoding;
@ -144,11 +151,11 @@ enc_new(rb_encoding *encoding)
static void static void
enc_list_update(int index, rb_raw_encoding *encoding) enc_list_update(int index, rb_raw_encoding *encoding)
{ {
ASSERT_GLOBAL_ENC_TABLE_LOCKED();
RUBY_ASSERT(index < ENCODING_LIST_CAPA); RUBY_ASSERT(index < ENCODING_LIST_CAPA);
VALUE list = rb_encoding_list; VALUE list = rb_encoding_list;
if (list && NIL_P(rb_ary_entry(list, index))) { if (list && NIL_P(rb_ary_entry(list, index))) {
RUBY_ASSERT(!rb_multi_ractor_p());
/* initialize encoding data */ /* initialize encoding data */
rb_ary_store(list, index, enc_new(encoding)); rb_ary_store(list, index, enc_new(encoding));
} }
@ -160,11 +167,9 @@ enc_list_lookup(int idx)
VALUE list, enc = Qnil; VALUE list, enc = Qnil;
if (idx < ENCODING_LIST_CAPA) { if (idx < ENCODING_LIST_CAPA) {
GLOBAL_ENC_TABLE_LOCKING(enc_table) { list = rb_encoding_list;
list = rb_encoding_list; RUBY_ASSERT(list);
RUBY_ASSERT(list); enc = rb_ary_entry(list, idx);
enc = rb_ary_entry(list, idx);
}
} }
if (NIL_P(enc)) { if (NIL_P(enc)) {
@ -206,10 +211,13 @@ static int
check_encoding(rb_encoding *enc) check_encoding(rb_encoding *enc)
{ {
int index = rb_enc_to_index(enc); int index = rb_enc_to_index(enc);
if (rb_enc_from_index(index) != enc) if (rb_enc_from_index(index) != enc) {
encdebug("check_encoding(%s): rb_enc_from_index(index) != enc, return -1\n", rb_enc_name(enc));
return -1; return -1;
}
if (rb_enc_autoload_p(enc)) { if (rb_enc_autoload_p(enc)) {
index = rb_enc_autoload(enc); index = rb_enc_autoload(enc);
encdebug("check_encoding(%s): rb_enc_autoload_p(enc), index after autoload:%d\n", rb_enc_name(enc), index);
} }
return index; return index;
} }
@ -340,7 +348,7 @@ rb_find_encoding(VALUE enc)
} }
static int static int
enc_table_expand(struct enc_table *enc_table, int newsize) enc_table_count_check(int newsize)
{ {
if (newsize > ENCODING_LIST_CAPA) { if (newsize > ENCODING_LIST_CAPA) {
rb_raise(rb_eEncodingError, "too many encoding (> %d)", ENCODING_LIST_CAPA); rb_raise(rb_eEncodingError, "too many encoding (> %d)", ENCODING_LIST_CAPA);
@ -348,46 +356,49 @@ enc_table_expand(struct enc_table *enc_table, int newsize)
return newsize; return newsize;
} }
// If called with a `base_encoding` of NULL, it is an autoloaded encoding
static int static int
enc_register_at(struct enc_table *enc_table, int index, const char *name, rb_encoding *base_encoding) enc_register_at(struct enc_table *enc_table, int index, const char *name, rb_encoding *base_encoding)
{ {
ASSERT_GLOBAL_ENC_TABLE_LOCKED();
struct rb_encoding_entry *ent = &enc_table->list[index]; struct rb_encoding_entry *ent = &enc_table->list[index];
rb_raw_encoding *encoding; rb_raw_encoding *encoding;
if (!valid_encoding_name_p(name)) return -1; if (!valid_encoding_name_p(name)) return -1;
if (!ent->name) { GLOBAL_ENC_TABLE_LOCKING(table) {
ent->name = name = strdup(name); if (!ent->name) {
ent->name = name = strdup(name);
}
else if (STRCASECMP(name, ent->name)) {
index = -1;
}
if (index != -1) {
encoding = (rb_raw_encoding *)ent->enc;
if (!encoding) {
encoding = xmalloc(sizeof(rb_encoding));
}
if (base_encoding) {
*encoding = *base_encoding;
}
else {
memset(encoding, 0, sizeof(*ent->enc));
}
encoding->name = name;
ent->enc = encoding;
st_insert(table->names, (st_data_t)name, (st_data_t)index);
enc_list_update(index, encoding);
encoding->ruby_encoding_index = index;
}
} }
else if (STRCASECMP(name, ent->name)) {
return -1;
}
encoding = (rb_raw_encoding *)ent->enc;
if (!encoding) {
encoding = xmalloc(sizeof(rb_encoding));
}
if (base_encoding) {
*encoding = *base_encoding;
}
else {
memset(encoding, 0, sizeof(*ent->enc));
}
encoding->name = name;
encoding->ruby_encoding_index = index;
ent->enc = encoding;
st_insert(enc_table->names, (st_data_t)name, (st_data_t)index);
enc_list_update(index, encoding);
return index; return index;
} }
static int static int
enc_register(struct enc_table *enc_table, const char *name, rb_encoding *encoding) enc_register(struct enc_table *enc_table, const char *name, rb_encoding *encoding)
{ {
ASSERT_GLOBAL_ENC_TABLE_LOCKED();
int index = enc_table->count; int index = enc_table->count;
ASSERT_GLOBAL_ENC_TABLE_LOCKED();
enc_table->count = enc_table_expand(enc_table, index + 1); enc_table->count = enc_table_count_check(index + 1);
return enc_register_at(enc_table, index, name, encoding); return enc_register_at(enc_table, index, name, encoding);
} }
@ -397,43 +408,30 @@ static int enc_registered(struct enc_table *enc_table, const char *name);
static rb_encoding * static rb_encoding *
enc_from_index(struct enc_table *enc_table, int index) enc_from_index(struct enc_table *enc_table, int index)
{ {
ASSERT_GLOBAL_ENC_TABLE_LOCKED();
return enc_table->list[index].enc; return enc_table->list[index].enc;
} }
// NOTE: there is no lock needed here, even with multi-ractor
rb_encoding * rb_encoding *
rb_enc_from_index(int index) rb_enc_from_index(int index)
{ {
rb_encoding *enc; rb_encoding *enc;
switch (index) { if (UNLIKELY(index < 0 || global_enc_table.count <= (index &= ENC_INDEX_MASK))) {
case ENCINDEX_US_ASCII: enc = NULL;
return global_enc_us_ascii;
case ENCINDEX_UTF_8:
return global_enc_utf_8;
case ENCINDEX_ASCII_8BIT:
return global_enc_ascii;
default:
break;
} }
GLOBAL_ENC_TABLE_LOCKING(enc_table) { else {
if (UNLIKELY(index < 0 || enc_table->count <= (index &= ENC_INDEX_MASK))) { enc = enc_from_index(&global_enc_table, index);
enc = NULL;
}
else {
enc = enc_from_index(enc_table, index);
}
} }
return enc; return enc;
} }
// This can be called from non-main ractors during autoloading of encodings
int int
rb_enc_register(const char *name, rb_encoding *encoding) rb_enc_register(const char *name, rb_encoding *encoding)
{ {
int index; int index;
unsigned int lev;
GLOBAL_ENC_TABLE_LOCK_ENTER_LEV(enc_table, &lev); GLOBAL_ENC_TABLE_LOCKING(enc_table) {
{
index = enc_registered(enc_table, name); index = enc_registered(enc_table, name);
if (index >= 0) { if (index >= 0) {
@ -445,7 +443,6 @@ rb_enc_register(const char *name, rb_encoding *encoding)
enc_register_at(enc_table, index, name, encoding); enc_register_at(enc_table, index, name, encoding);
} }
else { else {
GLOBAL_ENC_TABLE_LOCK_LEAVE_LEV(&lev);
rb_raise(rb_eArgError, "encoding %s is already registered", name); rb_raise(rb_eArgError, "encoding %s is already registered", name);
} }
} }
@ -454,14 +451,14 @@ rb_enc_register(const char *name, rb_encoding *encoding)
set_encoding_const(name, rb_enc_from_index(index)); set_encoding_const(name, rb_enc_from_index(index));
} }
} }
GLOBAL_ENC_TABLE_LOCK_LEAVE_LEV(&lev);
return index; return index;
} }
int static int
enc_registered(struct enc_table *enc_table, const char *name) enc_registered(struct enc_table *enc_table, const char *name)
{ {
st_data_t idx = 0; st_data_t idx = 0;
ASSERT_GLOBAL_ENC_TABLE_LOCKED(); ASSERT_GLOBAL_ENC_TABLE_LOCKED();
if (!name) return -1; if (!name) return -1;
@ -472,6 +469,7 @@ enc_registered(struct enc_table *enc_table, const char *name)
return -1; return -1;
} }
// Set up an encoding with a zeroed out entry->enc, which means it will be autoloaded
void void
rb_encdb_declare(const char *name) rb_encdb_declare(const char *name)
{ {
@ -498,7 +496,6 @@ enc_check_addable(struct enc_table *enc_table, const char *name)
static rb_encoding* static rb_encoding*
set_base_encoding(struct enc_table *enc_table, int index, rb_encoding *base) set_base_encoding(struct enc_table *enc_table, int index, rb_encoding *base)
{ {
ASSERT_GLOBAL_ENC_TABLE_LOCKED();
rb_encoding *enc = enc_table->list[index].enc; rb_encoding *enc = enc_table->list[index].enc;
ASSUME(enc); ASSUME(enc);
@ -565,6 +562,7 @@ enc_replicate_with_index(struct enc_table *enc_table, const char *name, rb_encod
return idx; return idx;
} }
// Setup a new encoding `name` that it a copy of `orig`, with a base of `orig`. Set `name` as dummy if `orig` is dummy.
int int
rb_encdb_replicate(const char *name, const char *orig) rb_encdb_replicate(const char *name, const char *orig)
{ {
@ -723,7 +721,7 @@ rb_encdb_alias(const char *alias, const char *orig)
static void static void
rb_enc_init(struct enc_table *enc_table) rb_enc_init(struct enc_table *enc_table)
{ {
enc_table_expand(enc_table, ENCODING_COUNT + 1); enc_table_count_check(ENCODING_COUNT + 1);
if (!enc_table->names) { if (!enc_table->names) {
enc_table->names = st_init_strcasetable_with_size(ENCODING_LIST_CAPA); enc_table->names = st_init_strcasetable_with_size(ENCODING_LIST_CAPA);
} }
@ -777,6 +775,7 @@ load_encoding(const char *name)
++s; ++s;
} }
enclib = rb_fstring(enclib); enclib = rb_fstring(enclib);
encdebug("load_encoding(%s)\n", RSTRING_PTR(enclib));
ruby_debug = Qfalse; ruby_debug = Qfalse;
errinfo = rb_errinfo(); errinfo = rb_errinfo();
loaded = rb_require_internal_silent(enclib); loaded = rb_require_internal_silent(enclib);
@ -784,15 +783,19 @@ load_encoding(const char *name)
rb_set_errinfo(errinfo); rb_set_errinfo(errinfo);
if (loaded < 0 || 1 < loaded) { if (loaded < 0 || 1 < loaded) {
encdebug("BAD load_encoding(%s): %d\n", name, loaded);
idx = -1; idx = -1;
} }
else if ((idx = enc_registered(&global_enc_table, name)) < 0) { else if ((idx = enc_registered(&global_enc_table, name)) < 0) {
encdebug("load_encoding(%s) after, enc_registered(name) < 0: %d\n", name, idx);
idx = -1; idx = -1;
} }
else if (rb_enc_autoload_p(global_enc_table.list[idx].enc)) { else if (rb_enc_autoload_p(global_enc_table.list[idx].enc)) {
encdebug("load_encoding(%s) after, enc_autoload_p still true\n", name);
idx = -1; idx = -1;
} }
encdebug("load_encoding(%s) returned index: %d\n", name, idx);
return idx; return idx;
} }
@ -811,6 +814,7 @@ enc_autoload_body(struct enc_table *enc_table, rb_encoding *enc)
if (rb_enc_autoload(base) < 0) return -1; if (rb_enc_autoload(base) < 0) return -1;
} }
i = enc->ruby_encoding_index; i = enc->ruby_encoding_index;
encdebug("enc_autoload_body of enc %s from base %s\n", rb_enc_name(enc), rb_enc_name(base));
enc_register_at(enc_table, i & ENC_INDEX_MASK, rb_enc_name(enc), base); enc_register_at(enc_table, i & ENC_INDEX_MASK, rb_enc_name(enc), base);
((rb_raw_encoding *)enc)->ruby_encoding_index = i; ((rb_raw_encoding *)enc)->ruby_encoding_index = i;
i &= ENC_INDEX_MASK; i &= ENC_INDEX_MASK;
@ -826,15 +830,25 @@ rb_enc_autoload(rb_encoding *enc)
{ {
int i; int i;
GLOBAL_ENC_TABLE_LOCKING(enc_table) { GLOBAL_ENC_TABLE_LOCKING(enc_table) {
i = enc_autoload_body(enc_table, enc); if (rb_enc_autoload_p(enc)) {
if (i == -2) { i = enc_autoload_body(enc_table, enc);
i = load_encoding(rb_enc_name(enc)); if (i == -2) {
i = load_encoding(rb_enc_name(enc));
encdebug("enc_autoload_body returned -2 (no base), loaded encoding %s, got ret i=%d\n", rb_enc_name(enc), i);
}
else if (i == -1) {
encdebug("enc_autoload_body returned -1 for encoding %s, autoload failed\n", rb_enc_name(enc));
}
}
else {
encdebug("already autoloaded: %s\n", rb_enc_name(enc));
i = check_encoding(enc);
} }
} }
return i; return i;
} }
/* Return encoding index or UNSPECIFIED_ENCODING from encoding name */ /* Return encoding index or UNSPECIFIED_ENCODING from encoding name. Loads autoloaded and unregistered encodings. */
int int
rb_enc_find_index(const char *name) rb_enc_find_index(const char *name)
{ {
@ -844,6 +858,7 @@ rb_enc_find_index(const char *name)
GLOBAL_ENC_TABLE_LOCKING(enc_table) { GLOBAL_ENC_TABLE_LOCKING(enc_table) {
i = enc_registered(enc_table, name); i = enc_registered(enc_table, name);
if (i < 0) { if (i < 0) {
encdebug("rb_enc_find_index not found, loading encoding %s\n", name);
i = load_encoding(name); i = load_encoding(name);
loaded_encoding = true; loaded_encoding = true;
} }
@ -864,7 +879,7 @@ rb_enc_find_index(const char *name)
if (rb_enc_autoload(enc) < 0) { if (rb_enc_autoload(enc) < 0) {
rb_warn("failed to load encoding (%s); use ASCII-8BIT instead", rb_warn("failed to load encoding (%s); use ASCII-8BIT instead",
name); name);
return 0; return ENCINDEX_ASCII_8BIT;
} }
} }
return i; return i;
@ -1584,14 +1599,15 @@ enc_set_default_encoding(struct default_encoding *def, VALUE encoding, const cha
{ {
int overridden = FALSE; int overridden = FALSE;
if (def->index != -2)
/* Already set */
overridden = TRUE;
GLOBAL_ENC_TABLE_LOCKING(enc_table) { GLOBAL_ENC_TABLE_LOCKING(enc_table) {
if (def->index != -2) {
/* Already set */
overridden = TRUE;
}
if (NIL_P(encoding)) { if (NIL_P(encoding)) {
RUBY_ASSERT(def != &default_external);
def->index = -1; def->index = -1;
def->enc = 0; RUBY_ATOMIC_PTR_SET(def->enc, 0);
char *name_dup = strdup(name); char *name_dup = strdup(name);
st_data_t existing_name = (st_data_t)name_dup; st_data_t existing_name = (st_data_t)name_dup;
@ -1603,9 +1619,10 @@ enc_set_default_encoding(struct default_encoding *def, VALUE encoding, const cha
(st_data_t)UNSPECIFIED_ENCODING); (st_data_t)UNSPECIFIED_ENCODING);
} }
else { else {
def->index = rb_enc_to_index(rb_to_encoding(encoding)); rb_encoding *enc = rb_to_encoding(encoding); // NOTE: this autoloads the encoding if necessary
def->enc = 0; def->index = rb_enc_to_index(enc);
enc_alias_internal(enc_table, name, def->index); enc_alias_internal(enc_table, name, def->index);
RUBY_ATOMIC_PTR_SET(def->enc, 0);
} }
if (def == &default_external) { if (def == &default_external) {
@ -1619,15 +1636,18 @@ enc_set_default_encoding(struct default_encoding *def, VALUE encoding, const cha
rb_encoding * rb_encoding *
rb_default_external_encoding(void) rb_default_external_encoding(void)
{ {
rb_encoding *enc = NULL; rb_encoding *enc;
// TODO: make lock-free enc = (rb_encoding*)RUBY_ATOMIC_PTR_LOAD(default_external.enc);
if (enc) {
return enc;
}
GLOBAL_ENC_TABLE_LOCKING(enc_table) { GLOBAL_ENC_TABLE_LOCKING(enc_table) {
if (default_external.enc) { if (default_external.enc) {
enc = default_external.enc; enc = default_external.enc;
} }
else if (default_external.index >= 0) { else if (default_external.index >= 0) {
default_external.enc = rb_enc_from_index(default_external.index); enc = rb_enc_from_index(default_external.index);
enc = default_external.enc; RUBY_ATOMIC_PTR_SET(default_external.enc, (void*)enc);
} }
else { else {
enc = rb_locale_encoding(); enc = rb_locale_encoding();
@ -1712,15 +1732,20 @@ static struct default_encoding default_internal = {-2};
rb_encoding * rb_encoding *
rb_default_internal_encoding(void) rb_default_internal_encoding(void)
{ {
rb_encoding *enc = NULL; rb_encoding *enc = RUBY_ATOMIC_PTR_LOAD(default_internal.enc);
// TODO: make lock-free if (enc) {
return enc;
}
else if (default_internal.index < 0) {
return NULL;
}
GLOBAL_ENC_TABLE_LOCKING(enc_table) { GLOBAL_ENC_TABLE_LOCKING(enc_table) {
if (!default_internal.enc && default_internal.index >= 0) { if (!default_internal.enc && default_internal.index >= 0) {
default_internal.enc = rb_enc_from_index(default_internal.index); enc = rb_enc_from_index(default_internal.index);
RUBY_ATOMIC_PTR_SET(default_internal.enc, (void*)enc);
} }
enc = default_internal.enc;
} }
return enc; /* can be NULL */ return enc;
} }
VALUE VALUE
@ -1962,6 +1987,7 @@ Init_Encoding(void)
{ {
VALUE list; VALUE list;
int i; int i;
encdebug("Init_Encoding\n");
rb_cEncoding = rb_define_class("Encoding", rb_cObject); rb_cEncoding = rb_define_class("Encoding", rb_cObject);
rb_define_alloc_func(rb_cEncoding, enc_s_alloc); rb_define_alloc_func(rb_cEncoding, enc_s_alloc);
@ -1993,6 +2019,7 @@ Init_Encoding(void)
RBASIC_CLEAR_CLASS(list); RBASIC_CLEAR_CLASS(list);
rb_vm_register_global_object(list); rb_vm_register_global_object(list);
encdebug("enc_table->count: %d\n", enc_table->count);
for (i = 0; i < enc_table->count; ++i) { for (i = 0; i < enc_table->count; ++i) {
rb_ary_push(list, enc_new(enc_table->list[i].enc)); rb_ary_push(list, enc_new(enc_table->list[i].enc));
} }
@ -2014,6 +2041,7 @@ Init_unicode_version(void)
void void
Init_encodings(void) Init_encodings(void)
{ {
encdebug("Init_encodings\n");
rb_enc_init(&global_enc_table); rb_enc_init(&global_enc_table);
} }

View file

@ -144,7 +144,9 @@ class TestEncoding < Test::Unit::TestCase
rs = [] rs = []
100.times do 100.times do
rs << Ractor.new do rs << Ractor.new do
"abc".force_encoding(Encoding.list.shuffle.first) 10_000.times do
"abc".force_encoding(Encoding.list.shuffle.first)
end
end end
end end
while rs.any? while rs.any?