From e51f9e9f75cc1dde9234836fa92077d71b3c5141 Mon Sep 17 00:00:00 2001 From: Satoshi Tagomori Date: Fri, 8 Dec 2023 15:06:12 +0900 Subject: [PATCH] rb_ext_resolve_symbol: C API to resolve and return externed symbols [Feature #20005] This is a C API for extensions to resolve and get function symbols of other extensions. Extensions can check the expected symbol is correctly loaded and accessible, and use it if it is available. Otherwise, extensions can raise their own error to guide users to setup their environments correctly and what's missing. --- common.mk | 1 + dln.c | 4 +- .../load/resolve_symbol_resolver/extconf.rb | 1 + .../resolve_symbol_resolver.c | 50 +++++++++++++++++ .../load/resolve_symbol_target/extconf.rb | 1 + .../resolve_symbol_target.c | 15 +++++ .../resolve_symbol_target.def | 4 ++ .../resolve_symbol_target.h | 4 ++ ext/-test-/load/stringify_symbols/extconf.rb | 1 + .../stringify_symbols/stringify_symbols.c | 29 ++++++++++ ext/-test-/load/stringify_target/extconf.rb | 1 + .../load/stringify_target/stringify_target.c | 15 +++++ .../stringify_target/stringify_target.def | 4 ++ .../load/stringify_target/stringify_target.h | 4 ++ include/ruby/internal/intern/load.h | 37 ++++++++++++ load.c | 56 ++++++++++++++++--- test/-ext-/load/test_resolve_symbol.rb | 24 ++++++++ test/-ext-/load/test_stringify_symbols.rb | 35 ++++++++++++ 18 files changed, 277 insertions(+), 9 deletions(-) create mode 100644 ext/-test-/load/resolve_symbol_resolver/extconf.rb create mode 100644 ext/-test-/load/resolve_symbol_resolver/resolve_symbol_resolver.c create mode 100644 ext/-test-/load/resolve_symbol_target/extconf.rb create mode 100644 ext/-test-/load/resolve_symbol_target/resolve_symbol_target.c create mode 100644 ext/-test-/load/resolve_symbol_target/resolve_symbol_target.def create mode 100644 ext/-test-/load/resolve_symbol_target/resolve_symbol_target.h create mode 100644 ext/-test-/load/stringify_symbols/extconf.rb create mode 100644 ext/-test-/load/stringify_symbols/stringify_symbols.c create mode 100644 ext/-test-/load/stringify_target/extconf.rb create mode 100644 ext/-test-/load/stringify_target/stringify_target.c create mode 100644 ext/-test-/load/stringify_target/stringify_target.def create mode 100644 ext/-test-/load/stringify_target/stringify_target.h create mode 100644 test/-ext-/load/test_resolve_symbol.rb create mode 100644 test/-ext-/load/test_stringify_symbols.rb 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