diff --git a/common.mk b/common.mk index 07875e31bf..51a8c71283 100644 --- a/common.mk +++ b/common.mk @@ -8542,6 +8542,7 @@ load.$(OBJEXT): $(top_srcdir)/internal/dir.h load.$(OBJEXT): $(top_srcdir)/internal/error.h load.$(OBJEXT): $(top_srcdir)/internal/file.h load.$(OBJEXT): $(top_srcdir)/internal/gc.h +load.$(OBJEXT): $(top_srcdir)/internal/hash.h load.$(OBJEXT): $(top_srcdir)/internal/imemo.h load.$(OBJEXT): $(top_srcdir)/internal/load.h load.$(OBJEXT): $(top_srcdir)/internal/parse.h diff --git a/dln.c b/dln.c index 9e01c45a51..fdad548448 100644 --- a/dln.c +++ b/dln.c @@ -463,8 +463,8 @@ dln_symbol(void *handle, const char *symbol) } if (handle == NULL) { # if defined(USE_DLN_DLOPEN) - handle = dlopen(NULL, 0); -# elif defined(_WIN32) && defined(RUBY_EXPORT) + handle = dlopen(NULL, RTLD_LAZY | RTLD_GLOBAL); +# elif defined(_WIN32) handle = rb_libruby_handle(); # else return NULL; diff --git a/ext/-test-/load/resolve_symbol_resolver/extconf.rb b/ext/-test-/load/resolve_symbol_resolver/extconf.rb new file mode 100644 index 0000000000..2299efcfd3 --- /dev/null +++ b/ext/-test-/load/resolve_symbol_resolver/extconf.rb @@ -0,0 +1 @@ +create_makefile('-test-/load/resolve_symbol_resolver') diff --git a/ext/-test-/load/resolve_symbol_resolver/resolve_symbol_resolver.c b/ext/-test-/load/resolve_symbol_resolver/resolve_symbol_resolver.c new file mode 100644 index 0000000000..a996e2e26e --- /dev/null +++ b/ext/-test-/load/resolve_symbol_resolver/resolve_symbol_resolver.c @@ -0,0 +1,50 @@ +#include +#include "ruby/internal/intern/load.h" + +typedef VALUE(*target_func)(VALUE); + +static target_func rst_any_method; + +VALUE +rsr_any_method(VALUE klass) +{ + return rst_any_method((VALUE)NULL); +} + +VALUE +rsr_try_resolve_fname(VALUE klass) +{ + target_func rst_something_missing = + (target_func) rb_ext_resolve_symbol("-test-/load/resolve_symbol_missing", "rst_any_method"); + if (rst_something_missing == NULL) { + // This should be done in Init_*, so the error is LoadError + rb_raise(rb_eLoadError, "symbol not found: missing fname"); + } + return Qtrue; +} + +VALUE +rsr_try_resolve_sname(VALUE klass) +{ + target_func rst_something_missing = + (target_func)rb_ext_resolve_symbol("-test-/load/resolve_symbol_target", "rst_something_missing"); + if (rst_something_missing == NULL) { + // This should be done in Init_*, so the error is LoadError + rb_raise(rb_eLoadError, "symbol not found: missing sname"); + } + return Qtrue; +} + +void +Init_resolve_symbol_resolver(void) +{ + VALUE mod = rb_define_module("ResolveSymbolResolver"); + rb_define_singleton_method(mod, "any_method", rsr_any_method, 0); + rb_define_singleton_method(mod, "try_resolve_fname", rsr_try_resolve_fname, 0); + rb_define_singleton_method(mod, "try_resolve_sname", rsr_try_resolve_sname, 0); + + rst_any_method = (target_func)rb_ext_resolve_symbol("-test-/load/resolve_symbol_target", "rst_any_method"); + if (rst_any_method == NULL) { + rb_raise(rb_eLoadError, "resolve_symbol_target is not loaded"); + } +} diff --git a/ext/-test-/load/resolve_symbol_target/extconf.rb b/ext/-test-/load/resolve_symbol_target/extconf.rb new file mode 100644 index 0000000000..b5a99ca7f1 --- /dev/null +++ b/ext/-test-/load/resolve_symbol_target/extconf.rb @@ -0,0 +1 @@ +create_makefile('-test-/load/resolve_symbol_target') diff --git a/ext/-test-/load/resolve_symbol_target/resolve_symbol_target.c b/ext/-test-/load/resolve_symbol_target/resolve_symbol_target.c new file mode 100644 index 0000000000..b5bc9e8ee0 --- /dev/null +++ b/ext/-test-/load/resolve_symbol_target/resolve_symbol_target.c @@ -0,0 +1,15 @@ +#include +#include "resolve_symbol_target.h" + +VALUE +rst_any_method(VALUE klass) +{ + return rb_str_new_cstr("from target"); +} + +void +Init_resolve_symbol_target(void) +{ + VALUE mod = rb_define_module("ResolveSymbolTarget"); + rb_define_singleton_method(mod, "any_method", rst_any_method, 0); +} diff --git a/ext/-test-/load/resolve_symbol_target/resolve_symbol_target.def b/ext/-test-/load/resolve_symbol_target/resolve_symbol_target.def new file mode 100644 index 0000000000..c2ed3610fe --- /dev/null +++ b/ext/-test-/load/resolve_symbol_target/resolve_symbol_target.def @@ -0,0 +1,4 @@ +LIBRARY resolve_symbol_target +EXPORTS + Init_resolve_symbol_target + rst_any_method diff --git a/ext/-test-/load/resolve_symbol_target/resolve_symbol_target.h b/ext/-test-/load/resolve_symbol_target/resolve_symbol_target.h new file mode 100644 index 0000000000..7d471bf360 --- /dev/null +++ b/ext/-test-/load/resolve_symbol_target/resolve_symbol_target.h @@ -0,0 +1,4 @@ +#include +#include "ruby/internal/dllexport.h" + +RUBY_EXTERN VALUE rst_any_method(VALUE); diff --git a/ext/-test-/load/stringify_symbols/extconf.rb b/ext/-test-/load/stringify_symbols/extconf.rb new file mode 100644 index 0000000000..ac39c15f09 --- /dev/null +++ b/ext/-test-/load/stringify_symbols/extconf.rb @@ -0,0 +1 @@ +create_makefile('-test-/load/stringify_symbols') diff --git a/ext/-test-/load/stringify_symbols/stringify_symbols.c b/ext/-test-/load/stringify_symbols/stringify_symbols.c new file mode 100644 index 0000000000..11a5ee3bc5 --- /dev/null +++ b/ext/-test-/load/stringify_symbols/stringify_symbols.c @@ -0,0 +1,29 @@ +#include +#include "ruby/internal/intern/load.h" +#include "ruby/util.h" + +#if SIZEOF_INTPTR_T == SIZEOF_LONG_LONG +# define UINTPTR2NUM ULL2NUM +#elif SIZEOF_INTPTR_T == SIZEOF_LONG +# define UINTPTR2NUM ULONG2NUM +#else +# define UINTPTR2NUM UINT2NUM +#endif + +static VALUE +stringify_symbol(VALUE klass, VALUE fname, VALUE sname) +{ + void *ptr = rb_ext_resolve_symbol(StringValueCStr(fname), StringValueCStr(sname)); + if (ptr == NULL) { + return Qnil; + } + uintptr_t uintptr = (uintptr_t)ptr; + return UINTPTR2NUM(uintptr); +} + +void +Init_stringify_symbols(void) +{ + VALUE mod = rb_define_module("StringifySymbols"); + rb_define_singleton_method(mod, "stringify_symbol", stringify_symbol, 2); +} diff --git a/ext/-test-/load/stringify_target/extconf.rb b/ext/-test-/load/stringify_target/extconf.rb new file mode 100644 index 0000000000..4aa201cb09 --- /dev/null +++ b/ext/-test-/load/stringify_target/extconf.rb @@ -0,0 +1 @@ +create_makefile('-test-/load/stringify_target') diff --git a/ext/-test-/load/stringify_target/stringify_target.c b/ext/-test-/load/stringify_target/stringify_target.c new file mode 100644 index 0000000000..ce09b8fd77 --- /dev/null +++ b/ext/-test-/load/stringify_target/stringify_target.c @@ -0,0 +1,15 @@ +#include +#include "stringify_target.h" + +VALUE +stt_any_method(VALUE klass) +{ + return rb_str_new_cstr("from target"); +} + +void +Init_stringify_target(void) +{ + VALUE mod = rb_define_module("StringifyTarget"); + rb_define_singleton_method(mod, "any_method", stt_any_method, 0); +} diff --git a/ext/-test-/load/stringify_target/stringify_target.def b/ext/-test-/load/stringify_target/stringify_target.def new file mode 100644 index 0000000000..89c2b762de --- /dev/null +++ b/ext/-test-/load/stringify_target/stringify_target.def @@ -0,0 +1,4 @@ +LIBRARY stringify_target +EXPORTS + Init_stringify_target + stt_any_method diff --git a/ext/-test-/load/stringify_target/stringify_target.h b/ext/-test-/load/stringify_target/stringify_target.h new file mode 100644 index 0000000000..5081f8cbd6 --- /dev/null +++ b/ext/-test-/load/stringify_target/stringify_target.h @@ -0,0 +1,4 @@ +#include +#include "ruby/internal/dllexport.h" + +RUBY_EXTERN VALUE stt_any_method(VALUE); diff --git a/include/ruby/internal/intern/load.h b/include/ruby/internal/intern/load.h index 288a16c2ec..9ceb98c2e4 100644 --- a/include/ruby/internal/intern/load.h +++ b/include/ruby/internal/intern/load.h @@ -176,6 +176,43 @@ VALUE rb_f_require(VALUE self, VALUE feature); */ VALUE rb_require_string(VALUE feature); +/** + * Resolves and returns a symbol of a function in the native extension + * specified by the feature and symbol names. Extensions will use this function + * to access the symbols provided by other native extensions. + * + * @param[in] feature Name of a feature, e.g. `"json"`. + * @param[in] symbol Name of a symbol defined by the feature. + * @return The resolved symbol of a function, defined and externed by the + * specified feature. It may be NULL if the feature is not loaded, + * the feature is not extension, or the symbol is not found. + */ +void *rb_ext_resolve_symbol(const char *feature, const char *symbol); + +/** + * This macro is to provide backwards compatibility. It provides a way to + * define function prototypes and resolving function symbols in a safe way. + * + * ```CXX + * // prototypes + * #ifdef HAVE_RB_EXT_RESOLVE_SYMBOL + * VALUE *(*other_extension_func)(VALUE,VALUE); + * #else + * VALUE other_extension_func(VALUE); + * #endif + * + * // in Init_xxx() + * #ifdef HAVE_RB_EXT_RESOLVE_SYMBOL + * other_extension_func = \ + * (VALUE(*)(VALUE,VALUE))rb_ext_resolve_symbol(fname, sym_name); + * if (other_extension_func == NULL) { + * // raise your own error + * } + * #endif + * ``` + */ +#define HAVE_RB_EXT_RESOLVE_SYMBOL 1 + /** * @name extension configuration * @{ diff --git a/load.c b/load.c index 4c95e7379b..35b80a6b4a 100644 --- a/load.c +++ b/load.c @@ -8,6 +8,7 @@ #include "internal/dir.h" #include "internal/error.h" #include "internal/file.h" +#include "internal/hash.h" #include "internal/load.h" #include "internal/ruby_parser.h" #include "internal/thread.h" @@ -18,12 +19,22 @@ #include "ruby/encoding.h" #include "ruby/util.h" -static VALUE ruby_dln_librefs; +static VALUE ruby_dln_libmap; #define IS_RBEXT(e) (strcmp((e), ".rb") == 0) #define IS_SOEXT(e) (strcmp((e), ".so") == 0 || strcmp((e), ".o") == 0) #define IS_DLEXT(e) (strcmp((e), DLEXT) == 0) +#if SIZEOF_VALUE <= SIZEOF_LONG +# define SVALUE2NUM(x) LONG2NUM((long)(x)) +# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LONG(x) +#elif SIZEOF_VALUE <= SIZEOF_LONG_LONG +# define SVALUE2NUM(x) LL2NUM((LONG_LONG)(x)) +# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LL(x) +#else +# error Need integer for VALUE +#endif + enum { loadable_ext_rb = (0+ /* .rb extension is the first in both tables */ 1) /* offset by rb_find_file_ext() */ @@ -1225,7 +1236,7 @@ require_internal(rb_execution_context_t *ec, VALUE fname, int exception, bool wa ec->errinfo = Qnil; /* ensure */ th->top_wrapper = 0; if ((state = EC_EXEC_TAG()) == TAG_NONE) { - long handle; + VALUE handle; int found; RUBY_DTRACE_HOOK(FIND_REQUIRE_ENTRY, RSTRING_PTR(fname)); @@ -1256,9 +1267,9 @@ require_internal(rb_execution_context_t *ec, VALUE fname, int exception, bool wa case 's': reset_ext_config = true; ext_config_push(th, &prev_ext_config); - handle = (long)rb_vm_call_cfunc(rb_vm_top_self(), load_ext, - path, VM_BLOCK_HANDLER_NONE, path); - rb_ary_push(ruby_dln_librefs, LONG2NUM(handle)); + handle = rb_vm_call_cfunc(rb_vm_top_self(), load_ext, + path, VM_BLOCK_HANDLER_NONE, path); + rb_hash_aset(ruby_dln_libmap, path, SVALUE2NUM((SIGNED_VALUE)handle)); break; } result = TAG_RETURN; @@ -1518,6 +1529,37 @@ rb_f_autoload_p(int argc, VALUE *argv, VALUE obj) return rb_mod_autoload_p(argc, argv, klass); } +void * +rb_ext_resolve_symbol(const char* fname, const char* symbol) +{ + VALUE handle; + VALUE resolved; + VALUE path; + char *ext; + VALUE fname_str = rb_str_new_cstr(fname); + + resolved = rb_resolve_feature_path((VALUE)NULL, fname_str); + if (NIL_P(resolved)) { + ext = strrchr(fname, '.'); + if (!ext || !IS_SOEXT(ext)) { + rb_str_cat_cstr(fname_str, ".so"); + } + if (rb_feature_p(GET_VM(), fname, 0, FALSE, FALSE, 0)) { + return dln_symbol(NULL, symbol); + } + return NULL; + } + if (RARRAY_LEN(resolved) != 2 || rb_ary_entry(resolved, 0) != ID2SYM(rb_intern("so"))) { + return NULL; + } + path = rb_ary_entry(resolved, 1); + handle = rb_hash_lookup(ruby_dln_libmap, path); + if (NIL_P(handle)) { + return NULL; + } + return dln_symbol((void *)NUM2SVALUE(handle), symbol); +} + void Init_load(void) { @@ -1552,6 +1594,6 @@ Init_load(void) rb_define_global_function("autoload", rb_f_autoload, 2); rb_define_global_function("autoload?", rb_f_autoload_p, -1); - ruby_dln_librefs = rb_ary_hidden_new(0); - rb_gc_register_mark_object(ruby_dln_librefs); + ruby_dln_libmap = rb_hash_new_with_size(0); + rb_gc_register_mark_object(ruby_dln_libmap); } diff --git a/test/-ext-/load/test_resolve_symbol.rb b/test/-ext-/load/test_resolve_symbol.rb new file mode 100644 index 0000000000..eeebc60679 --- /dev/null +++ b/test/-ext-/load/test_resolve_symbol.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +require 'test/unit' + +class Test_Load_ResolveSymbol < Test::Unit::TestCase + def test_load_resolve_symbol_resolver + feature = "Feature #20005" + assert_raise(LoadError, "resolve_symbol_target is not loaded") { + require '-test-/load/resolve_symbol_resolver' + } + require '-test-/load/resolve_symbol_target' + assert_nothing_raised(LoadError, "#{feature} resolver can be loaded") { + require '-test-/load/resolve_symbol_resolver' + } + assert_not_nil ResolveSymbolResolver + assert_equal "from target", ResolveSymbolResolver.any_method + + assert_raise(LoadError, "tries to resolve missing feature name, and it should raise LoadError") { + ResolveSymbolResolver.try_resolve_fname + } + assert_raise(LoadError, "tries to resolve missing symbol name, and it should raise LoadError") { + ResolveSymbolResolver.try_resolve_sname + } + end +end diff --git a/test/-ext-/load/test_stringify_symbols.rb b/test/-ext-/load/test_stringify_symbols.rb new file mode 100644 index 0000000000..0d9736b591 --- /dev/null +++ b/test/-ext-/load/test_stringify_symbols.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +require 'test/unit' + +class Test_Load_stringify_symbols < Test::Unit::TestCase + def test_load_stringify_symbol_required_extensions + require '-test-/load/stringify_symbols' + require '-test-/load/stringify_target' + r1 = StringifySymbols.stringify_symbol("-test-/load/stringify_target", "stt_any_method") + assert_not_nil r1 + r2 = StringifySymbols.stringify_symbol("-test-/load/stringify_target.so", "stt_any_method") + assert_equal r1, r2, "resolved symbols should be equal even with or without .so suffix" + end + + def test_load_stringify_symbol_statically_linked + require '-test-/load/stringify_symbols' + # "complex.so" is actually not a statically linked extension. + # But it is registered in $LOADED_FEATURES, so it can be a target of this test. + r1 = StringifySymbols.stringify_symbol("complex", "rb_complex_minus") + assert_not_nil r1 + r2 = StringifySymbols.stringify_symbol("complex.so", "rb_complex_minus") + assert_equal r1, r2 + end + + def test_load_stringify_symbol_missing_target + require '-test-/load/stringify_symbols' + r1 = assert_nothing_raised { + StringifySymbols.stringify_symbol("something_missing", "unknown_method") + } + assert_nil r1 + r2 = assert_nothing_raised { + StringifySymbols.stringify_symbol("complex.so", "unknown_method") + } + assert_nil r2 + end +end